Queer European MD passionate about IT

ciclopi.py 54 KB

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