diff --git a/Dockerfile b/Dockerfile
index 709b457..13c25c6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM python:3.9-slim
+FROM python:3.10-slim
LABEL version="2021" \
description="Vaalilakanabot" \
@@ -9,6 +9,6 @@ COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
COPY assets ./assets
-COPY vaalilakanabot2021.py vaalilakanabot2021.py
+COPY vaalilakanabot2022.py vaalilakanabot2022.py
-CMD ["python3", "vaalilakanabot2021.py"]
\ No newline at end of file
+CMD ["python3", "vaalilakanabot2022.py"]
diff --git a/README.md b/README.md
index 5375972..cb57ea9 100644
--- a/README.md
+++ b/README.md
@@ -1,46 +1 @@
-# Vaalilakanabot
-
-Telegram-botti, joka vaalien aikaan ylläpitää listausta ehdolle asettuneista henkilöistä ja ilmoittaa uusista postauksista killan Discourse-pohjaisella keskustelufoorumilla [Φrumilla](https://fiirumi.fyysikkokilta.fi).
-
-## Ominaisuudet
-- Ilmoittaa chatteissa, joihin botti on lisätty, aina kun fiirumille on tullut uusi postaus
-- Botin admin-käyttäjä voi ylläpitää sähköistä vaalilakanaa
-- Jauhistelu
-
-## Käyttöönotto
-- asenna `python-telegram-bot`-kirjasto (versio >=12) ja muut tarvittavat kirjastot
-- lisää Bot Fatherilta saatava `VAALILAKANABOT_TOKEN` ympäristönmuuttujaksi käyttöjärjestelmään.
-- täydennä ADMIN_CHAT_ID koodiin (halutun ryhmän id:n saa esimerkiksi lisäämällä botin `@RawDataBot` haluttuun ryhmään)
-- Päivitä TOPIC_LIST_URL ja QUESTION_LIST_URL -muuttujat koodiin. Katso [Discoursen dokumentaatio](https://docs.discourse.org/#tag/Categories/paths/~1c~1{id}.json/get) oikeanlaisen URL:n asettamiseksi.
-- `$ python vaalilakanabot2019.py`
-- lisää botti relevantteihin keskusteluryhmiin
-
-## Running the bot with Docker
-- create copies of the example_*.json files with such names that the "example_" part is removed.
-- create `bot.env` where `VAALILAKANABOT_TOKEN` and `ADMIN_CHAT_ID` env variables are stored
-- Update TOPIC_LIST_URL ja QUESTION_LIST_URL -variables in the code. See [Discourse documentation](https://docs.discourse.org/#tag/Categories/paths/~1c~1{id}.json/get) for formatting.
-- ```bash
- # Use this to run the development container (from dev branch in GitHub)
- docker-compose -f docker-compose.yml --profile dev up -d
-
- # Use this to run the production container (from master branch in GitHub)
- docker-compose -f docker-compose.yml --profile prod up -d
- ```
-
-## Komennot
-Botti tukee seuraavia komentoja:
-- `/start` Rekisteröi ryhmän botin tiedotuskanavaksi ja ryhmää saa botilta ilmoituksia.
-- `/jauhis` Näytää vaaliaiheisen kuvan.
-- `/lakana` Näytää vaalien ehdokastilanteen.
-
-Admin-chatissa seuraavat komennot ovat käytössä:
-- `/lisaa` Lisää ehdokkaan vaalilakanaan.
-- `/lisaa_fiirumi` Lisää ehdokkaan fiirumipostauksen vaalilakanaan.
-- `/poista` Poistaa ehdokkaan lakanasta.
-- `/valittu` Merkitsee vaalilakanassa ehdokkaan valituksi virkaan.
-- `/tiedota` Julkaisee uuden merkinnän vaalilakanassa.
-
-## Lisätietoa
-Lisää telegram-boteista voi lukea esimerkiksi [Kvantti I/19 s.22-25](https://kvantti.ayy.fi/blog/wp-content/uploads/2019/03/kvantti-19-1-nettiin.pdf).
-
-Botin tekivät [Einari Tuukkanen](https://github.com/EinariTuukkanen) ja Uula Ollila.
+Kopio fyysikkokillan vaalilakanabotista github.com/fyysikkokilta/vaalilakanabot
diff --git a/assets/jaauh1.png b/assets/jaauh1.png
new file mode 100644
index 0000000..0e345ad
Binary files /dev/null and b/assets/jaauh1.png differ
diff --git a/assets/jaauh2.png b/assets/jaauh2.png
new file mode 100644
index 0000000..55375ac
Binary files /dev/null and b/assets/jaauh2.png differ
diff --git a/docker-compose.yml b/docker-compose.yml
index 876d2cb..0fdecb8 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,22 +2,21 @@ version: '3.4'
services:
bot:
build: .
- image: ghcr.io/fyysikkokilta/vaalilakanabot:master
volumes:
- ./data:/bot/data:rw
- ./logs:/bot/logs:rw
env_file:
- bot.env
- profiles:
- - prod
+ # profiles:
+ # - prod
+ restart: always
- dev-bot:
- build: .
- image: ghcr.io/fyysikkokilta/vaalilakanabot:dev
- volumes:
- - ./data:/bot/data:rw
- - ./logs:/bot/logs:rw
- env_file:
- - bot.env
- profiles:
- - dev
\ No newline at end of file
+ # dev-bot:
+ # build: .
+ # volumes:
+ # - ./data:/bot/data:rw
+ # - ./logs:/bot/logs:rw
+ # env_file:
+ # - bot.env
+ # profiles:
+ # - dev
diff --git a/vaalilakanabot2022.py b/vaalilakanabot2022.py
new file mode 100644
index 0000000..9e6f127
--- /dev/null
+++ b/vaalilakanabot2022.py
@@ -0,0 +1,589 @@
+import json
+import os
+import time
+import random
+from glob import glob
+
+import logging
+import requests
+
+from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup
+from telegram.ext import Updater, MessageHandler, CommandHandler, \
+ Filters, ConversationHandler, CallbackContext, CallbackQueryHandler
+
+TOKEN = os.environ['VAALILAKANABOT_TOKEN']
+ADMIN_CHAT_ID = os.environ['ADMIN_CHAT_ID']
+
+BASE_URL = 'https://fiirumi.fyysikkokilta.fi'
+
+# TODO: update these to correspond current year discussion board
+TOPIC_LIST_URL = f'{BASE_URL}/c/vaalipeli-2022-esittelyt/l/latest.json'
+QUESTION_LIST_URL = f'{BASE_URL}/c/vaalipeli-2022-kysymykset/l/latest.json'
+
+BOARD = ['Puheenjohtaja', 'Varapuheenjohtaja', 'Rahastonhoitaja', 'Viestintävastaava',
+ 'IE', 'Hupimestari', 'Yrityssuhdevastaava', 'Kv-vastaava', 'Opintovastaava',
+ 'Fuksikapteeni']
+
+OFFICIALS = ['ISOvastaava', 'Jatkuvuustoimikunnan puheenjohtaja', 'Excumestari',
+ 'Lukkarimestari', 'Kvantin päätoimittaja']
+
+SELECTING_POSITION_CLASS, SELECTING_POSITION, TYPING_NAME, CONFIRMING = range(4)
+
+channels = []
+vaalilakana = {}
+last_applicant = None
+fiirumi_posts = []
+question_posts = []
+
+logger = logging.getLogger('vaalilakanabot')
+logger.setLevel(logging.INFO)
+
+log_path = os.path.join('logs', 'vaalilakanabot.log')
+fh = logging.FileHandler(log_path)
+fh.setLevel(logging.INFO)
+
+formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+fh.setFormatter(formatter)
+logger.addHandler(fh)
+
+with open('data/vaalilakana.json', 'r') as f:
+ data = f.read()
+ vaalilakana = json.loads(data)
+
+logger.info('Loaded vaalilakana: %s', vaalilakana)
+
+with open('data/channels.json', 'r') as f:
+ data = f.read()
+ channels = json.loads(data)
+
+logger.info('Loaded channels: %s', channels)
+
+with open('data/fiirumi_posts.json', 'r') as f:
+ data = f.read()
+ fiirumi_posts = json.loads(data)
+
+logger.info('Loaded fiirumi posts: %s', fiirumi_posts)
+
+with open('data/question_posts.json', 'r') as f:
+ data = f.read()
+ question_posts = json.loads(data)
+
+logger.info('Loaded question posts: %s', fiirumi_posts)
+
+updater = Updater(TOKEN, use_context=True)
+
+
+def _save_data(filename, content):
+ with open(filename, 'w') as fp:
+ fp.write(json.dumps(content))
+
+
+def _vaalilakana_to_string(lakana):
+ output = ''
+ output += '---------------Raati---------------\n'
+ # Hardcoded to maintain order instead using dict keys
+ for position in BOARD:
+ output += f'{position}:\n'
+ for applicant in lakana[position]:
+ link = applicant['fiirumi']
+ selected = applicant['valittu']
+ if selected:
+ if link:
+ output += f'- {applicant["name"]} (valittu)\n'
+ else:
+ output += f'- {applicant["name"]} (valittu)\n'
+ else:
+ if link:
+ output += f'- {applicant["name"]}\n'
+ else:
+ output += f'- {applicant["name"]}\n'
+
+ output += '\n'
+ output += '----------Toimihenkilöt----------\n'
+ for position in OFFICIALS:
+ output += f'{position}:\n'
+ for applicant in lakana[position]:
+ link = applicant['fiirumi']
+ selected = applicant['valittu']
+ if selected:
+ if link:
+ output += f'- {applicant["name"]} (valittu)\n'
+ else:
+ output += f'- {applicant["name"]} (valittu)\n'
+ else:
+ if link:
+ output += f'- {applicant["name"]}\n'
+ else:
+ output += f'- {applicant["name"]}\n'
+
+ output += '\n'
+ return output
+
+
+def parse_fiirumi_posts(context=updater.bot):
+ try:
+ page_fiirumi = requests.get(TOPIC_LIST_URL)
+ logger.debug(page_fiirumi)
+ page_question = requests.get(QUESTION_LIST_URL)
+ topic_list_raw = page_fiirumi.json()
+ logger.debug(str(topic_list_raw))
+ question_list_raw = page_question.json()
+ topic_list = topic_list_raw['topic_list']['topics']
+ question_list = question_list_raw['topic_list']['topics']
+
+ logger.debug(topic_list)
+ except KeyError as e:
+ logger.error("The topic and question lists cannot be found. Check URLs. Got error %s", e)
+ return
+ except Exception as e:
+ logger.error(e)
+ return
+
+ for topic in topic_list:
+ t_id = topic['id']
+ title = topic['title']
+ slug = topic['slug']
+ if str(t_id) not in fiirumi_posts:
+ new_post = {
+ 'id': t_id,
+ 'title': title,
+ 'slug': slug,
+ }
+ fiirumi_posts[str(t_id)] = new_post
+ _save_data('data/fiirumi_posts.json', fiirumi_posts)
+ _announce_to_channels(
+ f'Uusi postaus Vaalipeli-palstalla!\n{title}\n{BASE_URL}/t/{slug}/{t_id}'
+ )
+
+ for question in question_list:
+ t_id = question['id']
+ title = question['title']
+ slug = question['slug']
+ if str(t_id) not in question_posts:
+ new_question = {
+ 'id': t_id,
+ 'title': title,
+ 'slug': slug,
+ }
+ question_posts[str(t_id)] = new_question
+ _save_data('data/question_posts.json', question_posts)
+ _announce_to_channels(
+ f'Uusi kysymys Fiirumilla!\n{title}\n{BASE_URL}/t/{slug}/{t_id}'
+ )
+
+
+def _announce_to_channels(message):
+ for cid in channels:
+ try:
+ updater.bot.send_message(cid, message, parse_mode='HTML')
+ time.sleep(0.5)
+ except Exception as e:
+ logger.error(e)
+ continue
+
+
+def remove_applicant(update, context):
+ try:
+ chat_id = update.message.chat.id
+ if str(chat_id) == str(ADMIN_CHAT_ID):
+ text = update.message.text.replace('/poista', '').strip()
+ params = text.split(',')
+
+ try:
+ position = params[0].strip()
+ name = params[1].strip()
+ except Exception as e:
+ updater.bot.send_message(
+ chat_id,
+ 'Virheelliset parametrit - /poista , '
+ )
+ raise Exception('Invalid parameters') from e
+
+ if position not in vaalilakana:
+ updater.bot.send_message(
+ chat_id,
+ f'Tunnistamaton virka: {position}',
+ parse_mode='HTML'
+ )
+ raise Exception(f'Unknown position {position}')
+
+ found = None
+ for applicant in vaalilakana[position]:
+ if name == applicant['name']:
+ found = applicant
+ break
+
+ if not found:
+ updater.bot.send_message(
+ chat_id,
+ f'Hakijaa ei löydy {name}',
+ parse_mode='HTML'
+ )
+ raise Exception(f'Applicant not found: {name}')
+
+ vaalilakana[position].remove(found)
+ _save_data('data/vaalilakana.json', vaalilakana)
+ global last_applicant
+ last_applicant = None
+
+ updater.bot.send_message(
+ chat_id,
+ 'Poistettu:\n{position}: {name}'.format(
+ **found
+ ),
+ parse_mode='HTML'
+ )
+ except Exception as e:
+ logger.error(e)
+
+
+def add_fiirumi_to_applicant(update, context):
+ try:
+ chat_id = update.message.chat.id
+ if str(chat_id) == str(ADMIN_CHAT_ID):
+ text = update.message.text.replace('/lisaa_fiirumi', '').strip()
+ params = text.split(',')
+
+ try:
+ position = params[0].strip()
+ name = params[1].strip()
+ thread_id = params[2].strip()
+ except Exception as e:
+ updater.bot.send_message(
+ chat_id,
+ 'Virheelliset parametrit - /lisaa_fiirumi , , '
+ )
+ raise Exception('Invalid parameters') from e
+
+ if position not in vaalilakana:
+ updater.bot.send_message(
+ chat_id,
+ f'Tunnistamaton virka: {position}',
+ parse_mode='HTML'
+ )
+ raise Exception(f'Unknown position {position}')
+
+ if thread_id not in fiirumi_posts:
+ updater.bot.send_message(
+ chat_id,
+ f'Fiirumi-postausta ei löytynyt annetulla id:llä: {thread_id}',
+ parse_mode='HTML'
+ )
+ raise Exception(f'Unknown thread {thread_id}')
+
+ found = None
+ for applicant in vaalilakana[position]:
+ if name == applicant['name']:
+ found = applicant
+ fiirumi = f'{BASE_URL}/t/{fiirumi_posts[thread_id]["slug"]}/{fiirumi_posts[thread_id]["id"]}'
+ applicant['fiirumi'] = fiirumi
+ break
+
+ if not found:
+ updater.bot.send_message(
+ chat_id,
+ f'Hakijaa ei löydy {name}',
+ parse_mode='HTML'
+ )
+ raise Exception(f'Applicant not found: {name}')
+
+ _save_data('data/vaalilakana.json', vaalilakana)
+ global last_applicant
+ last_applicant = None
+
+ updater.bot.send_message(
+ chat_id,
+ 'Lisätty Fiirumi:\n{position}: {name}'.format(
+ fiirumi,
+ **found
+ ),
+ parse_mode='HTML'
+ )
+ except Exception as e:
+ logger.error(e)
+
+
+def add_applicant(update: Update, context: CallbackContext) -> None:
+ """Add an applicant. This command is for admin use."""
+ keyboard = [[
+ InlineKeyboardButton("Raatiin", callback_data='board'),
+ InlineKeyboardButton("Toimihenkilöksi", callback_data='official'),
+ ]]
+
+ reply_markup = InlineKeyboardMarkup(keyboard)
+
+ chat_id = update.message.chat.id
+ if str(chat_id) == str(ADMIN_CHAT_ID):
+ update.message.reply_text('Mihin rooliin henkilö lisätään?', reply_markup=reply_markup)
+ return SELECTING_POSITION_CLASS
+ else:
+ update.message.reply_text('Et oo admin :(((')
+ return None
+
+
+def generate_positions(position_class):
+ keyboard = []
+ for position in position_class:
+ keyboard.append(
+ [InlineKeyboardButton(position, callback_data=position)]
+ )
+ return keyboard
+
+
+def select_position_class(update: Update, context: CallbackContext) -> int:
+ """Parses the CallbackQuery and updates the message text."""
+ query = update.callback_query
+ query.answer()
+ chat_data = context.chat_data
+ logger.debug(str(chat_data))
+
+ # CallbackQueries need to be answered, even if no notification to the user is needed
+ # Some clients may have trouble otherwise. See https://core.telegram.org/bots/api#callbackquery
+ if query.data == '1':
+ logger.debug('Raati')
+ keyboard = InlineKeyboardMarkup(generate_positions(BOARD))
+ query.edit_message_reply_markup(keyboard)
+ return SELECTING_POSITION
+ elif query.data == '2':
+ logger.debug('Toimihenkilö')
+ keyboard = InlineKeyboardMarkup(generate_positions(OFFICIALS))
+ query.edit_message_reply_markup(keyboard)
+ return SELECTING_POSITION
+ else:
+ return SELECTING_POSITION_CLASS
+
+
+def select_board_position(update: Update, context: CallbackContext) -> int:
+ query = update.callback_query
+ query.answer()
+ keyboard = InlineKeyboardMarkup(generate_positions(BOARD))
+ query.edit_message_reply_markup(keyboard)
+ return SELECTING_POSITION
+
+
+def select_official_position(update: Update, context: CallbackContext) -> int:
+ query = update.callback_query
+ query.answer()
+ keyboard = InlineKeyboardMarkup(generate_positions(OFFICIALS))
+ query.edit_message_reply_markup(keyboard)
+ return SELECTING_POSITION
+
+
+def register_position(update: Update, context: CallbackContext) -> int:
+ """Parses the CallbackQuery and updates the message text."""
+ query = update.callback_query
+ chat_data = context.chat_data
+
+ # CallbackQueries need to be answered, even if no notification to the user is needed
+ # Some clients may have trouble otherwise. See https://core.telegram.org/bots/api#callbackquery
+ query.answer()
+ query.edit_message_text(text=f"Hakija rooliin: {query.data}\nKirjoita hakijan nimi vastauksena tähän viestiin")
+ chat_data['new_applicant_position'] = query.data
+ return TYPING_NAME
+
+
+def enter_applicant_name(update: Update, context: CallbackContext) -> int:
+ """Stores the info about the user and ends the conversation."""
+ chat_data = context.chat_data
+ logger.debug(chat_data)
+ name = update.message.text
+ logger.debug(name)
+ try:
+ chat_id = update.message.chat.id
+ position = chat_data['new_applicant_position']
+ chat_data['new_applicant_name'] = name
+
+ new_applicant = {'name': name, 'position': position, 'fiirumi': '', 'valittu': False}
+
+ vaalilakana[position].append(new_applicant)
+ _save_data('data/vaalilakana.json', vaalilakana)
+ global last_applicant
+ last_applicant = new_applicant
+
+ updater.bot.send_message(
+ chat_id,
+ 'Lisätty:\n{position}: {name}.\n\nLähetä tiedote komennolla /tiedota'.format(
+ **new_applicant
+ ),
+ parse_mode='HTML'
+ )
+ except Exception as e:
+ # TODO: Return to role selection
+ logger.error(e)
+ return ConversationHandler.END
+
+
+def add_selected_tag(update, context):
+ try:
+ chat_id = update.message.chat.id
+ if str(chat_id) == str(ADMIN_CHAT_ID):
+ text = update.message.text.replace('/valittu', '').strip()
+ params = text.split(',')
+
+ try:
+ position = params[0].strip()
+ name = params[1].strip()
+ except Exception as e:
+ updater.bot.send_message(
+ chat_id,
+ 'Virheelliset parametrit - /valittu , '
+ )
+ raise Exception from e
+
+ if position not in vaalilakana:
+ updater.bot.send_message(
+ chat_id,
+ f'Tunnistamaton virka: {position}',
+ parse_mode='HTML'
+ )
+ raise Exception(f'Unknown position {position}')
+
+ found = None
+ for applicant in vaalilakana[position]:
+ if name == applicant['name']:
+ found = applicant
+ applicant['valittu'] = True
+ break
+
+ if not found:
+ updater.bot.send_message(
+ chat_id,
+ f'Hakijaa ei löydy {name}',
+ parse_mode='HTML'
+ )
+ raise Exception(f'Applicant not found: {name}')
+
+ _save_data('data/vaalilakana.json', vaalilakana)
+ global last_applicant
+ last_applicant = None
+
+ updater.bot.send_message(
+ chat_id,
+ 'Hakija valittu:\n{position}: {name}'.format(
+ **found
+ ),
+ parse_mode='HTML'
+ )
+ except Exception as e:
+ logger.error(e)
+
+
+def show_vaalilakana(update, context):
+ try:
+ chat_id = update.message.chat.id
+ updater.bot.send_message(
+ chat_id,
+ _vaalilakana_to_string(vaalilakana),
+ parse_mode='HTML', disable_web_page_preview=True
+ )
+ except Exception as e:
+ logger.error(e)
+
+
+def register_channel(update, context):
+ try:
+ chat_id = update.message.chat.id
+ if chat_id not in channels:
+ channels.append(chat_id)
+ _save_data('data/channels.json', channels)
+ print(f'New channel added {chat_id}', update.message)
+ updater.bot.send_message(
+ chat_id,
+ 'Rekisteröity Vaalilakanabotin tiedotuskanavaksi!'
+ )
+ except Exception as e:
+ logger.error(e)
+
+
+def announce_new_applicant(update, context):
+ try:
+ chat_id = update.message.chat.id
+ if str(chat_id) == str(ADMIN_CHAT_ID):
+ global last_applicant
+ if last_applicant:
+ _announce_to_channels(
+ 'Uusi nimi vaalilakanassa!\n{position}: {name}'.format(
+ **last_applicant
+ )
+ )
+ last_applicant = None
+ except Exception as e:
+ logger.error(e)
+
+
+def jauhis(update, context):
+ try:
+ chat_id = update.message.chat.id
+ with open('assets/jauhis.png', 'rb') as photo:
+ updater.bot.send_sticker(chat_id, photo)
+ except Exception as e:
+ logger.warning("Error in sending Jauhis %s", e)
+
+
+def jauh(update, context):
+ try:
+ chat_id = update.message.chat.id
+ with open('assets/jauh.png', 'rb') as photo:
+ updater.bot.send_sticker(chat_id, photo)
+ except Exception as e:
+ logger.warning("Error in sending Jauh %s", e)
+
+def jaauh(update, context):
+ img = random.choice(glob('assets/jaauh*.png'))
+ try:
+ chat_id = update.message.chat.id
+ with open(img, 'rb') as photo:
+ updater.bot.send_sticker(chat_id, photo)
+ except Exception as e:
+ logger.warning("Error in sending Jaauh %s", e)
+
+def error(update, context):
+ """Log Errors caused by Updates."""
+ logger.warning('Update "%s" caused error "%s"', update, context.error)
+
+
+def cancel(update, context):
+ chat_data = context.chat_data
+ chat_data.clear()
+
+
+def main():
+ jq = updater.job_queue
+ jq.run_repeating(parse_fiirumi_posts, interval=60, first=0, context=updater.bot)
+
+ dp = updater.dispatcher
+
+ dp.add_handler(CommandHandler('valittu', add_selected_tag))
+ dp.add_handler(CommandHandler('lisaa_fiirumi', add_fiirumi_to_applicant))
+ dp.add_handler(CommandHandler('poista', remove_applicant))
+ dp.add_handler(CommandHandler('lakana', show_vaalilakana))
+ dp.add_handler(CommandHandler('tiedota', announce_new_applicant))
+ dp.add_handler(CommandHandler('start', register_channel))
+ dp.add_handler(CommandHandler('jauhis', jauhis))
+ dp.add_handler(CommandHandler('jauh', jauh))
+ dp.add_handler(CommandHandler('jaauh', jaauh))
+
+ conv_handler = ConversationHandler(
+ entry_points=[CommandHandler('lisaa', add_applicant)],
+ states={
+ SELECTING_POSITION_CLASS: [
+ CallbackQueryHandler(select_board_position, pattern='^board$'),
+ CallbackQueryHandler(select_official_position, pattern='^official$')
+ ],
+ SELECTING_POSITION: [
+ CallbackQueryHandler(register_position)
+ ],
+ TYPING_NAME: [MessageHandler(Filters.text & (~Filters.command), enter_applicant_name)],
+ },
+ fallbacks=[CommandHandler('cancel', cancel), CommandHandler('lisaa', add_applicant)],
+ )
+
+ dp.add_handler(conv_handler)
+
+ dp.add_error_handler(error)
+ updater.start_polling()
+ updater.idle()
+
+
+if __name__ == "__main__":
+ main()