Queer European MD passionate about IT

ciclopi.py 44 KB

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