Queer European MD passionate about IT

ciclopi.py 54 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679
  1. """Get information about bike sharing in Pisa.
  2. Available bikes in bike sharing stations.
  3. """
  4. # Standard library modules
  5. import asyncio
  6. import datetime
  7. import inspect
  8. import math
  9. from collections import OrderedDict
  10. # Third party modules
  11. from typing import Union
  12. import davtelepot
  13. from davtelepot.utilities import (
  14. async_wrapper, CachedPage, get_cleaned_text,
  15. line_drawing_unordered_list, make_button, make_inline_keyboard,
  16. make_lines_of_buttons
  17. )
  18. default_location = None
  19. _URL = "http://www.ciclopi.eu/frmLeStazioni.aspx"
  20. ciclopi_web_page = CachedPage.get(
  21. _URL,
  22. datetime.timedelta(seconds=15),
  23. mode='html'
  24. )
  25. UNIT_TO_KM = {
  26. 'km': 1,
  27. 'm': 1000,
  28. 'mi': 0.621371192,
  29. 'nmi': 0.539956803,
  30. 'ft': 3280.839895013,
  31. 'in': 39370.078740158
  32. }
  33. CICLOPI_SORTING_CHOICES = {
  34. 0: dict(
  35. id='center',
  36. symbol='🏛'
  37. ),
  38. 1: dict(
  39. id='alphabetical',
  40. symbol='🔤'
  41. ),
  42. 2: dict(
  43. id='position',
  44. symbol='🧭'
  45. ),
  46. 3: dict(
  47. id='custom',
  48. symbol='⭐️'
  49. )
  50. }
  51. CICLOPI_STATIONS_TO_SHOW = {
  52. -1: dict(
  53. id='fav',
  54. symbol='⭐️'
  55. ),
  56. 0: dict(
  57. id='all',
  58. symbol='💯'
  59. ),
  60. 3: dict(
  61. id='3',
  62. symbol='3️⃣'
  63. ),
  64. 5: dict(
  65. id='5',
  66. symbol='5️⃣'
  67. ),
  68. 10: dict(
  69. id='10',
  70. symbol='🔟'
  71. )
  72. }
  73. def haversine_distance(lat1, lon1, lat2, lon2, degrees='dec', unit='m'):
  74. """
  75. Calculate the great circle distance between two points on Earth.
  76. (specified in decimal degrees)
  77. """
  78. assert unit in UNIT_TO_KM, "Invalid distance unit of measurement!"
  79. assert degrees in ['dec', 'rad'], "Invalid angle unit of measurement!"
  80. # Convert decimal degrees to radians
  81. if degrees == 'dec':
  82. lon1, lat1, lon2, lat2 = map(
  83. math.radians,
  84. [lon1, lat1, lon2, lat2]
  85. )
  86. average_earth_radius = 6371.0088 * UNIT_TO_KM[unit]
  87. return (
  88. 2
  89. * average_earth_radius
  90. * math.asin(
  91. math.sqrt(
  92. math.sin((lat2 - lat1) * 0.5) ** 2
  93. + math.cos(lat1)
  94. * math.cos(lat2)
  95. * math.sin((lon2 - lon1) * 0.5) ** 2
  96. )
  97. )
  98. )
  99. class Location:
  100. """Location in world map."""
  101. def __init__(self, coordinates):
  102. """Check and set instance attributes."""
  103. assert type(coordinates) is tuple, "`coordinates` must be a tuple"
  104. assert (
  105. len(coordinates) == 2
  106. and all(type(c) is float for c in coordinates)
  107. ), "`coordinates` must be two floats"
  108. self._coordinates = coordinates
  109. @property
  110. def coordinates(self):
  111. """Return a tuple (latitude, longitude)."""
  112. return self._coordinates
  113. @property
  114. def latitude(self):
  115. """Return latitude."""
  116. return self._coordinates[0]
  117. @property
  118. def longitude(self):
  119. """Return longitude."""
  120. return self._coordinates[1]
  121. def get_distance(self, other, *args, **kwargs):
  122. """Return the distance between two `Location`s."""
  123. return haversine_distance(
  124. self.latitude, self.longitude,
  125. other.latitude, other.longitude,
  126. *args, **kwargs
  127. )
  128. class Station(Location):
  129. """CicloPi bike sharing station."""
  130. stations = {
  131. 1: dict(
  132. name='Aeroporto',
  133. coordinates=(43.699455, 10.400075),
  134. ),
  135. 2: dict(
  136. name='Stazione F.S.',
  137. coordinates=(43.708627, 10.399051),
  138. ),
  139. 3: dict(
  140. name='Comune Palazzo Blu',
  141. coordinates=(43.715541, 10.400505),
  142. ),
  143. 4: dict(
  144. name='Teatro Tribunale',
  145. coordinates=(43.716391, 10.405136),
  146. ),
  147. 5: dict(
  148. name='Borgo Stretto',
  149. coordinates=(43.718518, 10.402165),
  150. ),
  151. 6: dict(
  152. name='Polo Marzotto',
  153. coordinates=(43.719772, 10.407291),
  154. ),
  155. 7: dict(
  156. name='Duomo',
  157. coordinates=(43.722855, 10.391977),
  158. ),
  159. 8: dict(
  160. name='Pietrasantina',
  161. coordinates=(43.729020, 10.392726),
  162. ),
  163. 9: dict(
  164. name='Paparelli',
  165. coordinates=(43.724449, 10.410438),
  166. ),
  167. 10: dict(
  168. name='Pratale',
  169. coordinates=(43.7212554, 10.4180257),
  170. ),
  171. 11: dict(
  172. name='Ospedale Cisanello',
  173. coordinates=(43.705752, 10.441740),
  174. ),
  175. 12: dict(
  176. name='Sms Biblioteca',
  177. coordinates=(43.706565, 10.419136),
  178. ),
  179. 13: dict(
  180. name='Vittorio Emanuele',
  181. coordinates=(43.710182, 10.398751),
  182. ),
  183. 14: dict(
  184. name='Palacongressi',
  185. coordinates=(43.710014, 10.410232),
  186. ),
  187. 15: dict(
  188. name='Porta a Lucca',
  189. coordinates=(43.724247, 10.402269),
  190. ),
  191. 16: dict(
  192. name='Solferino',
  193. coordinates=(43.715698, 10.394999),
  194. ),
  195. 17: dict(
  196. name='San Rossore F.S.',
  197. coordinates=(43.718992, 10.384391),
  198. ),
  199. 18: dict(
  200. name='Guerrazzi',
  201. coordinates=(43.710358, 10.405337),
  202. ),
  203. 19: dict(
  204. name='Livornese',
  205. coordinates=(43.708114, 10.384021),
  206. ),
  207. 20: dict(
  208. name='Cavalieri',
  209. coordinates=(43.719856, 10.400194),
  210. ),
  211. 21: dict(
  212. name='M. Libertà',
  213. coordinates=(43.719821, 10.403021),
  214. ),
  215. 22: dict(
  216. name='Galleria Gerace',
  217. coordinates=(43.710791, 10.420456),
  218. ),
  219. 23: dict(
  220. name='C. Marchesi',
  221. coordinates=(43.714971, 10.419322),
  222. ),
  223. 24: dict(
  224. name='CNR-Praticelli',
  225. coordinates=(43.719256, 10.424012),
  226. ),
  227. 25: dict(
  228. name='Sesta Porta',
  229. coordinates=(43.709162, 10.395837),
  230. ),
  231. 26: dict(
  232. name='Qualconia',
  233. coordinates=(43.713011, 10.394458),
  234. ),
  235. 27: dict(
  236. name='Donatello',
  237. coordinates=(43.711715, 10.372480),
  238. ),
  239. 28: dict(
  240. name='Spadoni',
  241. coordinates=(43.716850, 10.391347),
  242. ),
  243. 29: dict(
  244. name='Nievo',
  245. coordinates=(43.738286, 10.400865),
  246. ),
  247. 30: dict(
  248. name='Cisanello',
  249. coordinates=(43.701159, 10.438863),
  250. ),
  251. 31: dict(
  252. name='Edificio 3',
  253. coordinates=(43.707869, 10.441698),
  254. ),
  255. 32: dict(
  256. name='Edificio 6',
  257. coordinates=(43.709046, 10.442541),
  258. ),
  259. 33: dict(
  260. name='Frascani',
  261. coordinates=(43.710157, 10.433339),
  262. ),
  263. 34: dict(
  264. name='Chiarugi',
  265. coordinates=(43.726244, 10.412882),
  266. ),
  267. 35: dict(
  268. name='Praticelli 2',
  269. coordinates=(43.719619, 10.427469),
  270. ),
  271. 36: dict(
  272. name='Carducci',
  273. coordinates=(43.726700, 10.420562),
  274. ),
  275. 37: dict(
  276. name='Garibaldi',
  277. coordinates=(43.718077, 10.418168),
  278. ),
  279. 38: dict(
  280. name='Silvestro',
  281. coordinates=(43.714128, 10.409065),
  282. ),
  283. 39: dict(
  284. name='Pardi',
  285. coordinates=(43.702273, 10.399793),
  286. ),
  287. }
  288. def __init__(self, id_=0, name='unknown', coordinates=(91.0, 181.0)):
  289. """Check and set instance attributes."""
  290. if id_ in self.__class__.stations:
  291. coordinates = self.__class__.stations[id_]['coordinates']
  292. name = self.__class__.stations[id_]['name']
  293. Location.__init__(self, coordinates)
  294. self._id = id_
  295. self._name = name
  296. self._active = True
  297. self._location = None
  298. self._description = ''
  299. self._distance = None
  300. self._bikes = 0
  301. self._free = 0
  302. @property
  303. def id(self):
  304. """Return station identification number."""
  305. return self._id
  306. @property
  307. def name(self):
  308. """Return station name."""
  309. return self._name
  310. @property
  311. def description(self):
  312. """Return station description."""
  313. return self._description
  314. @property
  315. def is_active(self):
  316. """Return True if station is active.
  317. Return False if there are no bikes and no available stalls or if
  318. station was marked as inactive.
  319. """
  320. if self.free == self.bikes == 0:
  321. return False
  322. return self._active
  323. @property
  324. def location(self):
  325. """Return location from which distance should be evaluated."""
  326. if self._location is None:
  327. return default_location
  328. return self._location
  329. @property
  330. def distance(self):
  331. """Return distance from `self.location`.
  332. If distance is not evaluated yet, do it and store the result.
  333. Otherwise, return stored value.
  334. """
  335. if self._distance is None:
  336. self._distance = self.get_distance(self.location)
  337. return self._distance
  338. @property
  339. def bikes(self):
  340. """Return number of available bikes."""
  341. return self._bikes
  342. @property
  343. def free(self):
  344. """Return number of free slots."""
  345. return self._free
  346. def set_active(self, active):
  347. """Change station status to `active`.
  348. `active` should be either `True` or `False`.
  349. """
  350. assert type(active) is bool, "`active` should be a boolean."
  351. self._active = active
  352. def set_description(self, description):
  353. """Change station description to `description`.
  354. `description` should be a string.
  355. """
  356. assert type(description) is str, "`description` should be a boolean."
  357. self._description = description
  358. def set_location(self, location):
  359. """Change station location to `location`.
  360. `location` should be a Location object.
  361. """
  362. assert (
  363. isinstance(location, Location)
  364. ), "`location` should be a Location."
  365. self._location = location
  366. def set_bikes(self, bikes):
  367. """Change number of available `bikes`.
  368. `bikes` should be an int.
  369. """
  370. assert (
  371. type(bikes) is int
  372. ), "`bikes` should be an int."
  373. self._bikes = bikes
  374. def set_free(self, free):
  375. """Change number of `free` bike parking slots.
  376. `free` should be an int.
  377. """
  378. assert (
  379. type(free) is int
  380. ), "`free` should be an int."
  381. self._free = free
  382. @property
  383. def status(self):
  384. """Return station status to be shown to users.
  385. It includes distance, location, available bikes and free stalls.
  386. """
  387. if self.bikes + self.free == 0:
  388. bikes_and_stalls = "<i>⚠️ {{not_available}}</i>"
  389. else:
  390. bikes_and_stalls = f"🚲 {self.bikes} | 🅿️ {self.free}"
  391. return (
  392. f"<b>{self.name}</b> | <i>{self.description}</i>\n"
  393. f"<code> </code>{bikes_and_stalls} | 📍 {self.distance:.0f} m"
  394. ).format(
  395. s=self
  396. )
  397. def ciclopi_custom_sorter(custom_order):
  398. """Return a function to sort stations by a `custom_order`."""
  399. custom_values = {
  400. record['station']: record['value']
  401. for record in custom_order
  402. }
  403. def sorter(station):
  404. """Take a station and return its queue value.
  405. Stations will be sorted by queue value in ascending order.
  406. """
  407. if station.id in custom_values:
  408. return custom_values[station.id], station.name
  409. return 100, station.name
  410. return sorter
  411. def _get_stations(data, location):
  412. stations = []
  413. for _station in data.find_all(
  414. "li",
  415. attrs={"class": "rrItem"}
  416. ):
  417. station_name = _station.find(
  418. "span",
  419. attrs={"class": "Stazione"}
  420. ).text
  421. if 'Non operativa' in station_name:
  422. active = False
  423. else:
  424. active = True
  425. station_id = _station.find(
  426. "div",
  427. attrs={"class": "cssNumero"}
  428. ).text
  429. if (
  430. station_id is None
  431. or type(station_id) is not str
  432. or not station_id.isnumeric()
  433. ):
  434. station_id = 0
  435. else:
  436. station_id = int(station_id)
  437. station = Station(station_id)
  438. station.set_active(active)
  439. station.set_description(
  440. _station.find(
  441. "span",
  442. attrs={"class": "TableComune"}
  443. ).text.replace(
  444. 'a`',
  445. 'à'
  446. ).replace(
  447. 'e`',
  448. 'è'
  449. )
  450. )
  451. bikes_text = _station.find(
  452. "span",
  453. attrs={"class": "Red"}
  454. ).get_text('\t')
  455. if bikes_text.count('\t') < 1:
  456. bikes = 0
  457. free = 0
  458. else:
  459. bikes, free, *other = [
  460. int(
  461. ''.join(
  462. char
  463. for char in s
  464. if char.isnumeric()
  465. )
  466. )
  467. for s in bikes_text.split('\t')
  468. ]
  469. station.set_bikes(bikes)
  470. station.set_free(free)
  471. station.set_location(location)
  472. stations.append(
  473. station
  474. )
  475. return stations
  476. async def set_ciclopi_location(bot: davtelepot.bot.Bot,
  477. update: dict, user_record: OrderedDict,
  478. language: str):
  479. """Take a location update and store it as CicloPi place.
  480. CicloPi stations will be sorted by distance from this place.
  481. """
  482. location = update['location']
  483. chat_id = update['chat']['id']
  484. telegram_id = update['from']['id']
  485. with bot.db as db:
  486. db['ciclopi'].upsert(
  487. dict(
  488. chat_id=chat_id,
  489. latitude=location['latitude'],
  490. longitude=location['longitude']
  491. ),
  492. ['chat_id']
  493. )
  494. await bot.send_message(
  495. chat_id=chat_id,
  496. text=bot.get_message(
  497. 'ciclopi', 'set_position', 'success',
  498. update=update, user_record=user_record
  499. )
  500. )
  501. # Remove individual text message handler which was set to catch `/cancel`
  502. bot.remove_individual_text_message_handler(telegram_id)
  503. return await _ciclopi_command(bot=bot, update=update,
  504. user_record=user_record, language=language)
  505. async def cancel_ciclopi_location(bot, update, user_record):
  506. """Handle the situation in which a user does not send location on request.
  507. This function is set as individual text message handler when the bot
  508. requests user's location and is removed if user does send one.
  509. If not, return a proper message.
  510. """
  511. text = get_cleaned_text(bot=bot, update=update)
  512. # If user cancels operation, confirm that it was cancelled
  513. if text.lower() == 'annulla':
  514. return bot.get_message(
  515. 'ciclopi', 'set_position', 'cancel',
  516. update=update, user_record=user_record
  517. )
  518. # If user writes something else, remind them how to set position later
  519. return bot.get_message(
  520. 'ciclopi', 'set_position', 'cancel_and_remind',
  521. update=update, user_record=user_record
  522. )
  523. async def _ciclopi_command(bot: davtelepot.bot.Bot, update: dict,
  524. user_record: OrderedDict,
  525. language: str,
  526. sent_message=None,
  527. show_all=False):
  528. if ('ciclopi' not in bot.shared_data
  529. or 'is_working' not in bot.shared_data['ciclopi']
  530. or not bot.shared_data['ciclopi']['is_working']):
  531. return bot.get_message('ciclopi', 'service_unavailable',
  532. language=language)
  533. chat_id = update['chat']['id']
  534. default_stations_to_show = 5
  535. stations = []
  536. placeholder_id = bot.set_placeholder(
  537. timeout=datetime.timedelta(seconds=1),
  538. chat_id=chat_id,
  539. # sent_message=sent_message,
  540. # text="<i>{message}...</i>".format(
  541. # message=bot.get_message(
  542. # 'ciclopi', 'command', 'updating',
  543. # update=update, user_record=user_record
  544. # )
  545. # )
  546. )
  547. ciclopi_data = await ciclopi_web_page.get_page()
  548. if ciclopi_data is None or isinstance(ciclopi_data, Exception):
  549. text = bot.get_message(
  550. 'ciclopi', 'command', 'unavailable_website',
  551. update=update, user_record=user_record
  552. )
  553. else:
  554. with bot.db as db:
  555. ciclopi_record = db['ciclopi'].find_one(
  556. chat_id=chat_id
  557. )
  558. custom_order = list(
  559. db['ciclopi_custom_order'].find(
  560. chat_id=chat_id
  561. )
  562. )
  563. if (
  564. ciclopi_record is not None
  565. and isinstance(ciclopi_record, dict)
  566. and 'sorting' in ciclopi_record
  567. and ciclopi_record['sorting'] in CICLOPI_SORTING_CHOICES
  568. ):
  569. sorting_code = ciclopi_record['sorting']
  570. if (
  571. 'latitude' in ciclopi_record
  572. and ciclopi_record['latitude'] is not None
  573. and 'longitude' in ciclopi_record
  574. and ciclopi_record['longitude'] is not None
  575. ):
  576. saved_place = Location(
  577. (
  578. ciclopi_record['latitude'],
  579. ciclopi_record['longitude']
  580. )
  581. )
  582. else:
  583. saved_place = default_location
  584. else:
  585. sorting_code = 0
  586. if (
  587. ciclopi_record is not None
  588. and isinstance(ciclopi_record, dict)
  589. and 'stations_to_show' in ciclopi_record
  590. and ciclopi_record['stations_to_show'] in CICLOPI_STATIONS_TO_SHOW
  591. ):
  592. stations_to_show = ciclopi_record[
  593. 'stations_to_show'
  594. ]
  595. else:
  596. stations_to_show = default_stations_to_show
  597. location = (
  598. saved_place if sorting_code != 0
  599. else default_location
  600. )
  601. sorting_method = (
  602. (lambda station: station.distance) if sorting_code in [0, 2]
  603. else (lambda station: station.name) if sorting_code == 1
  604. else ciclopi_custom_sorter(custom_order) if sorting_code == 3
  605. else (lambda station: 0)
  606. )
  607. stations = sorted(
  608. _get_stations(
  609. ciclopi_data,
  610. location
  611. ),
  612. key=sorting_method
  613. )
  614. if (
  615. stations_to_show == -1
  616. and not show_all
  617. ):
  618. stations = list(
  619. filter(
  620. lambda station: station.id in [
  621. record['station']
  622. for record in custom_order
  623. ],
  624. stations
  625. )
  626. )
  627. if (
  628. stations_to_show > 0
  629. and sorting_code != 1
  630. and not show_all
  631. ):
  632. stations = stations[:stations_to_show]
  633. filter_label = ""
  634. if stations_to_show == -1:
  635. filter_label = bot.get_message(
  636. 'ciclopi', 'filters', 'fav', 'all' if show_all else 'only',
  637. update=update, user_record=user_record
  638. )
  639. elif len(stations) < len(Station.stations):
  640. filter_label = bot.get_message(
  641. 'ciclopi', 'filters', 'num',
  642. update=update, user_record=user_record,
  643. n=stations_to_show
  644. )
  645. if filter_label:
  646. filter_label = ' ({label})'.format(
  647. label=filter_label
  648. )
  649. text = (
  650. "🚲 {title} {order}"
  651. "{filter} {sort[symbol]}\n"
  652. "\n"
  653. "{stations_list}"
  654. ).format(
  655. title=bot.get_message(
  656. 'ciclopi', 'command', 'title',
  657. update=update, user_record=user_record
  658. ),
  659. sort=CICLOPI_SORTING_CHOICES[sorting_code],
  660. order=bot.get_message(
  661. 'ciclopi', 'sorting',
  662. CICLOPI_SORTING_CHOICES[sorting_code]['id'],
  663. 'short_description',
  664. update=update, user_record=user_record
  665. ),
  666. filter=filter_label,
  667. stations_list=(
  668. '\n\n'.join(
  669. station.status.format(
  670. not_available=bot.get_message(
  671. 'ciclopi', 'status', 'not_available',
  672. update=update, user_record=user_record
  673. )
  674. )
  675. for station in stations
  676. ) if len(stations)
  677. else "<i>- {message} -</i>".format(
  678. message=bot.get_message(
  679. 'ciclopi', 'command', 'no_station_available',
  680. update=update, user_record=user_record
  681. )
  682. )
  683. ),
  684. )
  685. if not text:
  686. return
  687. reply_markup = make_inline_keyboard(
  688. (
  689. [
  690. make_button(
  691. text="💯 {message}".format(
  692. message=bot.get_message(
  693. 'ciclopi', 'command', 'buttons', 'all',
  694. update=update, user_record=user_record
  695. )
  696. ),
  697. prefix='ciclopi:///',
  698. data=['show', 'all']
  699. )
  700. ] if len(stations) < len(Station.stations)
  701. else [
  702. make_button(
  703. "{sy} {message}".format(
  704. message=(
  705. bot.get_message(
  706. 'ciclopi', 'command', 'buttons', 'only_fav',
  707. update=update, user_record=user_record
  708. ) if stations_to_show == -1
  709. else bot.get_message(
  710. 'ciclopi', 'command', 'buttons', 'first_n',
  711. update=update, user_record=user_record,
  712. n=stations_to_show
  713. )
  714. ),
  715. sy=CICLOPI_STATIONS_TO_SHOW[stations_to_show]['symbol']
  716. ),
  717. prefix='ciclopi:///',
  718. data=['show']
  719. )
  720. ] if show_all
  721. else []
  722. ) + [
  723. make_button(
  724. text=bot.get_message(
  725. 'ciclopi', 'command', 'buttons', 'update',
  726. update=update, user_record=user_record
  727. ),
  728. prefix='ciclopi:///',
  729. data=(
  730. ['show'] + (
  731. [] if len(stations) < len(Station.stations)
  732. else ['all']
  733. )
  734. )
  735. ),
  736. make_button(
  737. text=bot.get_message(
  738. 'ciclopi', 'command', 'buttons', 'legend',
  739. update=update, user_record=user_record
  740. ),
  741. prefix='ciclopi:///',
  742. data=['legend']
  743. ),
  744. make_button(
  745. text=bot.get_message(
  746. 'ciclopi', 'command', 'buttons', 'settings',
  747. update=update, user_record=user_record
  748. ),
  749. prefix='ciclopi:///',
  750. data=['main']
  751. )
  752. ],
  753. 2
  754. )
  755. parameters = dict(
  756. update=update,
  757. text=text,
  758. parse_mode='HTML',
  759. reply_markup=reply_markup
  760. )
  761. method = (
  762. bot.send_message
  763. if sent_message is None
  764. else bot.edit_message_text
  765. )
  766. await method(**parameters)
  767. # Mark request as done
  768. bot.placeholder_requests[placeholder_id] = 1
  769. return
  770. def get_menu_back_buttons(bot, update, user_record,
  771. include_back_to_settings=True):
  772. """Return a list of menu buttons to navigate back in the menu.
  773. `include_back_to_settings` : Bool
  774. Set it to True to include a 'back to settings' menu button.
  775. """
  776. if include_back_to_settings:
  777. buttons = [
  778. make_button(
  779. text="⚙️ {message}".format(
  780. message=bot.get_message(
  781. 'ciclopi', 'button', 'back_to_settings',
  782. update=update, user_record=user_record
  783. )
  784. ),
  785. prefix='ciclopi:///',
  786. data=['main']
  787. )
  788. ]
  789. else:
  790. buttons = []
  791. buttons += [
  792. make_button(
  793. text="🚲 {message}".format(
  794. message=bot.get_message(
  795. 'ciclopi', 'button', 'back_to_stations',
  796. update=update, user_record=user_record
  797. )
  798. ),
  799. prefix='ciclopi:///',
  800. data=['show']
  801. )
  802. ]
  803. return buttons
  804. async def _ciclopi_button_main(bot, update, user_record):
  805. result, text, reply_markup = '', '', None
  806. text = (
  807. "⚙️ {settings_title} 🚲\n"
  808. "\n"
  809. "{settings_list}"
  810. ).format(
  811. settings_title=bot.get_message(
  812. 'ciclopi', 'button', 'title',
  813. update=update, user_record=user_record
  814. ),
  815. settings_list='\n'.join(
  816. "- {symbol} {name}: {description}".format(
  817. symbol=bot.get_message(
  818. 'ciclopi', 'settings', setting, 'symbol',
  819. update=update, user_record=user_record
  820. ),
  821. name=bot.get_message(
  822. 'ciclopi', 'settings', setting, 'name',
  823. update=update, user_record=user_record
  824. ),
  825. description=bot.get_message(
  826. 'ciclopi', 'settings', setting, 'description',
  827. update=update, user_record=user_record
  828. )
  829. )
  830. for setting in bot.messages['ciclopi']['settings']
  831. )
  832. )
  833. reply_markup = make_inline_keyboard(
  834. [
  835. make_button(
  836. text="{symbol} {name}".format(
  837. symbol=bot.get_message(
  838. 'ciclopi', 'settings', setting, 'symbol',
  839. update=update, user_record=user_record
  840. ),
  841. name=bot.get_message(
  842. 'ciclopi', 'settings', setting, 'name',
  843. update=update, user_record=user_record
  844. )
  845. ),
  846. prefix='ciclopi:///',
  847. data=[setting]
  848. )
  849. for setting in bot.messages['ciclopi']['settings']
  850. ] + get_menu_back_buttons(
  851. bot=bot, update=update, user_record=user_record,
  852. include_back_to_settings=False
  853. )
  854. )
  855. return result, text, reply_markup
  856. async def _ciclopi_button_sort(bot, update, user_record, arguments):
  857. result, text, reply_markup = '', '', None
  858. chat_id = (
  859. update['message']['chat']['id'] if 'message' in update
  860. else update['chat']['id'] if 'chat' in update
  861. else 0
  862. )
  863. with bot.db as db:
  864. ciclopi_record = db['ciclopi'].find_one(
  865. chat_id=chat_id
  866. )
  867. if ciclopi_record is None:
  868. ciclopi_record = dict(
  869. chat_id=chat_id,
  870. sorting=0
  871. )
  872. if len(arguments) == 1:
  873. new_choice = (
  874. arguments[0]
  875. if type(arguments[0]) is int
  876. else 0
  877. )
  878. if new_choice == ciclopi_record['sorting']:
  879. return bot.get_message(
  880. 'ciclopi', 'button', 'no_change',
  881. update=update, user_record=user_record
  882. ), '', None
  883. elif new_choice not in CICLOPI_SORTING_CHOICES:
  884. return bot.get_message(
  885. 'ciclopi', 'button', 'unknown_option',
  886. update=update, user_record=user_record
  887. ), '', None
  888. db['ciclopi'].upsert(
  889. dict(
  890. chat_id=chat_id,
  891. sorting=new_choice
  892. ),
  893. ['chat_id'],
  894. ensure=True
  895. )
  896. ciclopi_record['sorting'] = new_choice
  897. result = bot.get_message(
  898. 'ciclopi', 'button', 'done',
  899. update=update, user_record=user_record
  900. )
  901. text = bot.get_message(
  902. 'ciclopi', 'button', 'sorting_header',
  903. update=update, user_record=user_record
  904. ).format(
  905. options='\n'.join(
  906. "- {symbol} {name}: {description}".format(
  907. symbol=choice['symbol'],
  908. name=bot.get_message(
  909. 'ciclopi', 'sorting', choice['id'], 'name',
  910. update=update, user_record=user_record
  911. ),
  912. description=bot.get_message(
  913. 'ciclopi', 'sorting', choice['id'], 'description',
  914. update=update, user_record=user_record
  915. )
  916. )
  917. for choice in CICLOPI_SORTING_CHOICES.values()
  918. )
  919. )
  920. reply_markup = make_inline_keyboard(
  921. [
  922. make_button(
  923. text="{s} {name} {c[symbol]}".format(
  924. c=choice,
  925. s=(
  926. '✅'
  927. if code == ciclopi_record['sorting']
  928. else '☑️'
  929. ),
  930. name=bot.get_message(
  931. 'ciclopi', 'sorting', choice['id'], 'name',
  932. update=update, user_record=user_record
  933. )
  934. ),
  935. prefix='ciclopi:///',
  936. data=['sort', code]
  937. )
  938. for code, choice in CICLOPI_SORTING_CHOICES.items()
  939. ] + get_menu_back_buttons(
  940. bot=bot, update=update, user_record=user_record,
  941. include_back_to_settings=True
  942. )
  943. )
  944. return result, text, reply_markup
  945. async def _ciclopi_button_limit(bot, update, user_record, arguments):
  946. result, text, reply_markup = '', '', None
  947. chat_id = (
  948. update['message']['chat']['id'] if 'message' in update
  949. else update['chat']['id'] if 'chat' in update
  950. else 0
  951. )
  952. with bot.db as db:
  953. ciclopi_record = db['ciclopi'].find_one(
  954. chat_id=chat_id
  955. )
  956. if ciclopi_record is None or 'stations_to_show' not in ciclopi_record:
  957. ciclopi_record = dict(
  958. chat_id=chat_id,
  959. stations_to_show=5
  960. )
  961. if len(arguments) == 1:
  962. new_choice = (
  963. arguments[0]
  964. if type(arguments[0]) is int
  965. else int(arguments[0])
  966. if type(arguments[0]) is str and arguments[0].lstrip('+-').isnumeric()
  967. else 0
  968. )
  969. if new_choice == ciclopi_record['stations_to_show']:
  970. return bot.get_message(
  971. 'ciclopi', 'button', 'no_change',
  972. update=update, user_record=user_record
  973. ), '', None
  974. elif new_choice not in CICLOPI_STATIONS_TO_SHOW:
  975. return bot.get_message(
  976. 'ciclopi', 'button', 'unknown_option',
  977. update=update, user_record=user_record
  978. ), '', None
  979. db['ciclopi'].upsert(
  980. dict(
  981. chat_id=chat_id,
  982. stations_to_show=new_choice
  983. ),
  984. ['chat_id'],
  985. ensure=True
  986. )
  987. ciclopi_record['stations_to_show'] = new_choice
  988. result = bot.get_message(
  989. 'ciclopi', 'button', 'done',
  990. update=update, user_record=user_record
  991. )
  992. text = bot.get_message(
  993. 'ciclopi', 'button', 'limit_header',
  994. update=update, user_record=user_record
  995. ).format(
  996. options='\n'.join(
  997. "- {symbol} {name}".format(
  998. symbol=choice['symbol'],
  999. name=bot.get_message(
  1000. 'ciclopi', 'filters', choice['id'], 'name',
  1001. update=update, user_record=user_record
  1002. )
  1003. )
  1004. for choice in CICLOPI_STATIONS_TO_SHOW.values()
  1005. )
  1006. )
  1007. reply_markup = make_inline_keyboard(
  1008. [
  1009. make_button(
  1010. text="{s} {name} {symbol}".format(
  1011. symbol=choice['symbol'],
  1012. name=bot.get_message(
  1013. 'ciclopi', 'filters', choice['id'], 'name',
  1014. update=update, user_record=user_record
  1015. ),
  1016. s=(
  1017. '✅'
  1018. if code == ciclopi_record['stations_to_show']
  1019. else '☑️'
  1020. )
  1021. ),
  1022. prefix='ciclopi:///',
  1023. data=['limit', code]
  1024. )
  1025. for code, choice in CICLOPI_STATIONS_TO_SHOW.items()
  1026. ] + get_menu_back_buttons(
  1027. bot=bot, update=update, user_record=user_record,
  1028. include_back_to_settings=True
  1029. )
  1030. )
  1031. return result, text, reply_markup
  1032. async def _ciclopi_button_show(bot: davtelepot.bot.Bot, update: dict,
  1033. user_record: OrderedDict, language: str,
  1034. arguments: list):
  1035. result, text, reply_markup = '', '', None
  1036. fake_update = update['message']
  1037. fake_update['from'] = update['from']
  1038. asyncio.ensure_future(
  1039. _ciclopi_command(
  1040. bot=bot,
  1041. update=fake_update,
  1042. user_record=user_record,
  1043. sent_message=fake_update,
  1044. show_all=(
  1045. True if len(arguments) == 1 and arguments[0] == 'all'
  1046. else False
  1047. ),
  1048. language=language
  1049. )
  1050. )
  1051. return result, text, reply_markup
  1052. async def _ciclopi_button_legend(bot, update, user_record):
  1053. result, text, reply_markup = '', '', None
  1054. text = (
  1055. "<b>{s[name]}</b> | <i>{s[description]}</i>\n"
  1056. "<code> </code>🚲 {s[bikes]} | 🅿️ {s[free]} | 📍 {s[distance]}"
  1057. ).format(
  1058. s={
  1059. key: bot.get_message(
  1060. 'ciclopi', 'button', 'legend', key,
  1061. update=update, user_record=user_record
  1062. )
  1063. for key in ('name', 'distance', 'description', 'bikes', 'free')
  1064. }
  1065. )
  1066. reply_markup = make_inline_keyboard(
  1067. get_menu_back_buttons(
  1068. bot=bot, update=update, user_record=user_record,
  1069. include_back_to_settings=True
  1070. )
  1071. )
  1072. return result, text, reply_markup
  1073. async def _ciclopi_button_favourites_add(bot, update, user_record, arguments,
  1074. order_record, ordered_stations):
  1075. result = bot.get_message(
  1076. 'ciclopi', 'button', 'favourites', 'popup',
  1077. update=update, user_record=user_record
  1078. )
  1079. if len(arguments) == 2 and type(arguments[1]) is int:
  1080. station_id = int(arguments[1])
  1081. chat_id = (
  1082. update['message']['chat']['id'] if 'message' in update
  1083. else update['chat']['id'] if 'chat' in update
  1084. else 0
  1085. )
  1086. with bot.db as db:
  1087. if station_id in (s.id for s in ordered_stations): # Remove
  1088. # Find `old_record` to be removed
  1089. for old_record in order_record:
  1090. if old_record['station'] == station_id:
  1091. break
  1092. db.query(
  1093. """UPDATE ciclopi_custom_order
  1094. SET value = value - 1
  1095. WHERE chat_id = {chat_id}
  1096. AND value > {val}
  1097. """.format(
  1098. chat_id=chat_id,
  1099. val=old_record['value']
  1100. )
  1101. )
  1102. db['ciclopi_custom_order'].delete(
  1103. id=old_record['id']
  1104. )
  1105. ordered_stations = list(
  1106. filter(
  1107. (lambda s: s.id != station_id),
  1108. ordered_stations
  1109. )
  1110. )
  1111. else: # Add
  1112. new_record = dict(
  1113. chat_id=chat_id,
  1114. station=station_id,
  1115. value=(len(order_record) + 1)
  1116. )
  1117. db['ciclopi_custom_order'].upsert(
  1118. new_record,
  1119. ['chat_id', 'station'],
  1120. ensure=True
  1121. )
  1122. order_record.append(new_record)
  1123. ordered_stations.append(
  1124. Station(station_id)
  1125. )
  1126. text = bot.get_message(
  1127. 'ciclopi', 'button', 'favourites', 'header',
  1128. update=update, user_record=user_record,
  1129. options=line_drawing_unordered_list(
  1130. [
  1131. station.name
  1132. for station in ordered_stations
  1133. ]
  1134. )
  1135. )
  1136. reply_markup = dict(
  1137. inline_keyboard=make_lines_of_buttons(
  1138. [
  1139. make_button(
  1140. text=(
  1141. "{sy} {n}"
  1142. ).format(
  1143. sy=(
  1144. '✅' if station_id in [
  1145. s.id for s in ordered_stations
  1146. ]
  1147. else '☑️'
  1148. ),
  1149. n=station['name']
  1150. ),
  1151. prefix='ciclopi:///',
  1152. data=['fav', 'add', station_id]
  1153. )
  1154. for station_id, station in sorted(
  1155. Station.stations.items(),
  1156. key=lambda t: t[1]['name'] # Sort by station_name
  1157. )
  1158. ],
  1159. 3
  1160. ) + make_lines_of_buttons(
  1161. [
  1162. make_button(
  1163. text=bot.get_message(
  1164. 'ciclopi', 'button', 'favourites', 'sort', 'buttons',
  1165. 'change_order',
  1166. update=update, user_record=user_record
  1167. ),
  1168. prefix="ciclopi:///",
  1169. data=["fav"]
  1170. )
  1171. ] + get_menu_back_buttons(
  1172. bot=bot, update=update, user_record=user_record,
  1173. include_back_to_settings=True
  1174. ),
  1175. 3
  1176. )
  1177. )
  1178. return result, text, reply_markup
  1179. def move_favorite_station(
  1180. bot, chat_id, action, station_id,
  1181. order_record
  1182. ):
  1183. """Move a station in `chat_id`-associated custom order.
  1184. `bot`: Bot object, having a `.db` property.
  1185. `action`: should be `up` or `down`
  1186. `order_record`: list of records about `chat_id`-associated custom order.
  1187. """
  1188. assert action in ('up', 'down'), "Invalid action!"
  1189. for old_record in order_record:
  1190. if old_record['station'] == station_id:
  1191. break
  1192. else: # Error: no record found
  1193. return
  1194. with bot.db as db:
  1195. if action == 'down':
  1196. db.query(
  1197. """UPDATE ciclopi_custom_order
  1198. SET value = 500
  1199. WHERE chat_id = {chat_id}
  1200. AND value = {val} + 1
  1201. """.format(
  1202. chat_id=chat_id,
  1203. val=old_record['value']
  1204. )
  1205. )
  1206. db.query(
  1207. """UPDATE ciclopi_custom_order
  1208. SET value = value + 1
  1209. WHERE chat_id = {chat_id}
  1210. AND value = {val}
  1211. """.format(
  1212. chat_id=chat_id,
  1213. val=old_record['value']
  1214. )
  1215. )
  1216. db.query(
  1217. """UPDATE ciclopi_custom_order
  1218. SET value = {val}
  1219. WHERE chat_id = {chat_id}
  1220. AND value = 500
  1221. """.format(
  1222. chat_id=chat_id,
  1223. val=old_record['value']
  1224. )
  1225. )
  1226. elif action == 'up':
  1227. db.query(
  1228. """UPDATE ciclopi_custom_order
  1229. SET value = 500
  1230. WHERE chat_id = {chat_id}
  1231. AND value = {val} - 1
  1232. """.format(
  1233. chat_id=chat_id,
  1234. val=old_record['value']
  1235. )
  1236. )
  1237. db.query(
  1238. """UPDATE ciclopi_custom_order
  1239. SET value = value - 1
  1240. WHERE chat_id = {chat_id}
  1241. AND value = {val}
  1242. """.format(
  1243. chat_id=chat_id,
  1244. val=old_record['value']
  1245. )
  1246. )
  1247. db.query(
  1248. """UPDATE ciclopi_custom_order
  1249. SET value = {val}
  1250. WHERE chat_id = {chat_id}
  1251. AND value = 500
  1252. """.format(
  1253. chat_id=chat_id,
  1254. val=old_record['value']
  1255. )
  1256. )
  1257. order_record = list(
  1258. db['ciclopi_custom_order'].find(
  1259. chat_id=chat_id,
  1260. order_by=['value']
  1261. )
  1262. )
  1263. ordered_stations = [
  1264. Station(record['station'])
  1265. for record in order_record
  1266. ]
  1267. return order_record, ordered_stations
  1268. async def _ciclopi_button_favourites(bot, update, user_record, arguments):
  1269. result, text, reply_markup = '', '', None
  1270. action = (
  1271. arguments[0] if len(arguments) > 0
  1272. else 'up'
  1273. )
  1274. chat_id = (
  1275. update['message']['chat']['id'] if 'message' in update
  1276. else update['chat']['id'] if 'chat' in update
  1277. else 0
  1278. )
  1279. with bot.db as db:
  1280. order_record = list(
  1281. db['ciclopi_custom_order'].find(
  1282. chat_id=chat_id,
  1283. order_by=['value']
  1284. )
  1285. )
  1286. ordered_stations = [
  1287. Station(record['station'])
  1288. for record in order_record
  1289. ]
  1290. if action == 'add':
  1291. return await _ciclopi_button_favourites_add(
  1292. bot, update, user_record, arguments,
  1293. order_record, ordered_stations
  1294. )
  1295. elif action == 'dummy':
  1296. return bot.get_message(
  1297. 'ciclopi', 'button', 'favourites', 'sort', 'end',
  1298. update=update, user_record=user_record
  1299. ), '', None
  1300. elif action == 'set' and len(arguments) > 1:
  1301. action = arguments[1]
  1302. elif (
  1303. action in ['up', 'down']
  1304. and len(arguments) > 1
  1305. and type(arguments[1]) is int
  1306. ):
  1307. station_id = int(arguments[1])
  1308. order_record, ordered_stations = move_favorite_station(
  1309. bot, chat_id, action, station_id,
  1310. order_record
  1311. )
  1312. text = bot.get_message(
  1313. 'ciclopi', 'button', 'favourites', 'sort', 'header',
  1314. update=update, user_record=user_record,
  1315. options=line_drawing_unordered_list(
  1316. [
  1317. station.name
  1318. for station in ordered_stations
  1319. ]
  1320. )
  1321. )
  1322. reply_markup = dict(
  1323. inline_keyboard=[
  1324. [
  1325. make_button(
  1326. text="{s.name} {sy}".format(
  1327. sy=(
  1328. '⬆️' if (
  1329. action == 'up'
  1330. and n != 1
  1331. ) else '⬇️' if (
  1332. action == 'down'
  1333. and n != len(ordered_stations)
  1334. ) else '⏹'
  1335. ),
  1336. s=station
  1337. ),
  1338. prefix='ciclopi:///',
  1339. data=[
  1340. 'fav',
  1341. (
  1342. action if (
  1343. action == 'up'
  1344. and n != 1
  1345. ) or (
  1346. action == 'down'
  1347. and n != len(ordered_stations)
  1348. )
  1349. else 'dummy'
  1350. ),
  1351. station.id
  1352. ]
  1353. )
  1354. ]
  1355. for n, station in enumerate(ordered_stations, 1)
  1356. ] + [
  1357. [
  1358. make_button(
  1359. text=bot.get_message(
  1360. 'ciclopi', 'button', 'favourites', 'sort', 'buttons',
  1361. 'edit',
  1362. update=update, user_record=user_record
  1363. ),
  1364. prefix='ciclopi:///',
  1365. data=['fav', 'add']
  1366. )
  1367. ]
  1368. ] + [
  1369. [
  1370. (
  1371. make_button(
  1372. text=bot.get_message(
  1373. 'ciclopi', 'button', 'favourites', 'sort',
  1374. 'buttons', 'move_down',
  1375. update=update, user_record=user_record
  1376. ),
  1377. prefix='ciclopi:///',
  1378. data=['fav', 'set', 'down']
  1379. ) if action == 'up'
  1380. else make_button(
  1381. text=bot.get_message(
  1382. 'ciclopi', 'button', 'favourites', 'sort',
  1383. 'buttons', 'move_up',
  1384. update=update, user_record=user_record
  1385. ),
  1386. prefix='ciclopi:///',
  1387. data=['fav', 'set', 'up']
  1388. )
  1389. )
  1390. ]
  1391. ] + [
  1392. get_menu_back_buttons(
  1393. bot=bot, update=update, user_record=user_record,
  1394. include_back_to_settings=True
  1395. )
  1396. ]
  1397. )
  1398. return result, text, reply_markup
  1399. async def _ciclopi_button_setpos(bot, update, user_record, language):
  1400. result, text, reply_markup = '', '', None
  1401. chat_id = (
  1402. update['message']['chat']['id'] if 'message' in update
  1403. else update['chat']['id'] if 'chat' in update
  1404. else 0
  1405. )
  1406. result = bot.get_message(
  1407. 'ciclopi', 'button', 'location', 'popup',
  1408. update=update, user_record=user_record
  1409. )
  1410. bot.set_individual_location_handler(
  1411. await async_wrapper(
  1412. set_ciclopi_location,
  1413. language=language
  1414. ),
  1415. update
  1416. )
  1417. bot.set_individual_text_message_handler(
  1418. cancel_ciclopi_location,
  1419. update
  1420. )
  1421. asyncio.ensure_future(
  1422. bot.send_message(
  1423. chat_id=chat_id,
  1424. text=bot.get_message(
  1425. 'ciclopi', 'button', 'location', 'instructions',
  1426. update=update, user_record=user_record
  1427. ),
  1428. reply_markup=dict(
  1429. keyboard=[
  1430. [
  1431. dict(
  1432. text=bot.get_message(
  1433. 'ciclopi', 'button', 'location',
  1434. 'send_current_location',
  1435. update=update, user_record=user_record
  1436. ),
  1437. request_location=True
  1438. )
  1439. ],
  1440. [
  1441. dict(
  1442. text=bot.get_message(
  1443. 'ciclopi', 'button', 'location', 'cancel',
  1444. update=update, user_record=user_record
  1445. ),
  1446. )
  1447. ]
  1448. ],
  1449. resize_keyboard=True
  1450. )
  1451. )
  1452. )
  1453. return result, text, reply_markup
  1454. _ciclopi_button_routing_table = {
  1455. 'main': _ciclopi_button_main,
  1456. 'sort': _ciclopi_button_sort,
  1457. 'limit': _ciclopi_button_limit,
  1458. 'show': _ciclopi_button_show,
  1459. 'setpos': _ciclopi_button_setpos,
  1460. 'legend': _ciclopi_button_legend,
  1461. 'fav': _ciclopi_button_favourites
  1462. }
  1463. async def _ciclopi_button(bot: davtelepot.bot.Bot, update: dict,
  1464. user_record: OrderedDict, language: str,
  1465. data: list):
  1466. command, *arguments = data
  1467. if command in _ciclopi_button_routing_table:
  1468. handler = _ciclopi_button_routing_table[command]
  1469. parameters = {
  1470. name: value
  1471. for name, value in {'bot': bot,
  1472. 'update': update,
  1473. 'user_record': user_record,
  1474. 'language': language,
  1475. 'arguments': arguments
  1476. }.items()
  1477. if name in inspect.signature(handler).parameters
  1478. }
  1479. result, text, reply_markup = await handler(**parameters)
  1480. else:
  1481. return
  1482. if text:
  1483. return dict(
  1484. text=result,
  1485. edit=dict(
  1486. text=text,
  1487. parse_mode='HTML',
  1488. reply_markup=reply_markup
  1489. )
  1490. )
  1491. return result
  1492. async def check_service_status(bot: davtelepot.bot.Bot,
  1493. interval: Union[int, datetime.timedelta] = 60 * 60):
  1494. """Every `interval` seconds, check whether service is active or not.
  1495. Store service status in `bot.shared_data['ciclopi']`.
  1496. """
  1497. # TODO: adapt interval to events (check 1 minute after first
  1498. if isinstance(interval, datetime.timedelta):
  1499. interval = interval.total_seconds()
  1500. while 1:
  1501. ciclopi_data = await ciclopi_web_page.get_page()
  1502. if ciclopi_data is not None and not isinstance(ciclopi_data, Exception):
  1503. stations = _get_stations(
  1504. data=ciclopi_data,
  1505. location=default_location
  1506. )
  1507. bot.shared_data['ciclopi']['is_working'] = any(
  1508. station.is_active
  1509. for station in stations
  1510. )
  1511. await asyncio.sleep(interval)
  1512. def init(telegram_bot: davtelepot.bot.Bot, ciclopi_messages=None,
  1513. _default_location=(43.718518, 10.402165)):
  1514. """Take a bot and assign CicloPi-related commands to it.
  1515. `ciclopi_messages` : dict
  1516. Multilanguage dictionary with all CicloPi-related messages.
  1517. `default_location` : tuple (float, float)
  1518. Tuple of coordinates (latitude, longitude) of default location.
  1519. Defaults to Borgo Stretto CicloPi station.
  1520. """
  1521. # Define a global `default_location` variable holding default location
  1522. global default_location
  1523. default_location = Location(_default_location)
  1524. if 'ciclopi' not in telegram_bot.shared_data:
  1525. telegram_bot.shared_data['ciclopi'] = dict()
  1526. telegram_bot.shared_data['ciclopi']['default_location'] = default_location
  1527. asyncio.ensure_future(check_service_status(bot=telegram_bot))
  1528. db = telegram_bot.db
  1529. if 'ciclopi_stations' not in db.tables:
  1530. db['ciclopi_stations'].insert_many(
  1531. sorted(
  1532. [
  1533. dict(
  1534. station_id=station_id,
  1535. name=station['name'],
  1536. latitude=station['coordinates'][0],
  1537. longitude=station['coordinates'][1]
  1538. )
  1539. for station_id, station in Station.stations.items()
  1540. ],
  1541. key=(lambda station: station['station_id'])
  1542. )
  1543. )
  1544. if 'ciclopi' not in db.tables:
  1545. db['ciclopi'].insert(
  1546. dict(
  1547. chat_id=0,
  1548. sorting=0,
  1549. latitude=0.0,
  1550. longitude=0.0,
  1551. stations_to_show=-1
  1552. )
  1553. )
  1554. if ciclopi_messages is None:
  1555. try:
  1556. from .messages import default_ciclopi_messages as ciclopi_messages
  1557. except ImportError:
  1558. ciclopi_messages = {}
  1559. telegram_bot.messages['ciclopi'] = ciclopi_messages
  1560. @telegram_bot.command(command='/ciclopi', aliases=["CicloPi 🚲", "🚲 CicloPi 🔴"],
  1561. reply_keyboard_button=(
  1562. telegram_bot.messages['ciclopi']['command']['reply_keyboard_button']
  1563. ),
  1564. show_in_keyboard=True,
  1565. description=(
  1566. telegram_bot.messages['ciclopi']['command']['description']
  1567. ),
  1568. help_section=telegram_bot.messages['ciclopi']['help'],
  1569. authorization_level='everybody')
  1570. async def ciclopi_command(bot: davtelepot.bot.Bot, update: dict,
  1571. user_record: OrderedDict, language: str):
  1572. return await _ciclopi_command(bot=bot, update=update,
  1573. user_record=user_record, language=language)
  1574. @telegram_bot.button(prefix='ciclopi:///', separator='|', authorization_level='everybody')
  1575. async def ciclopi_button(bot: davtelepot.bot.Bot, update: dict,
  1576. user_record: OrderedDict, language: str,
  1577. data: list):
  1578. return await _ciclopi_button(bot=bot, update=update,
  1579. user_record=user_record, language=language,
  1580. data=data)