Queer European MD passionate about IT

ciclopi.py 52 KB

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