diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..73672ce --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.env + +logs/ +!example* \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0fb0238 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.9-slim + +WORKDIR /bot +COPY requirements.txt requirements.txt +RUN pip install -r requirements.txt + +COPY assets ./assets +COPY vaalilakanabot2021.py vaalilakanabot2021.py + +CMD ["python3", "vaalilakanabot2021.py"] \ No newline at end of file diff --git a/jauhis.png b/assets/jauhis.png similarity index 100% rename from jauhis.png rename to assets/jauhis.png diff --git a/channels.json b/data/channels.json similarity index 100% rename from channels.json rename to data/channels.json diff --git a/fiirumi_posts.json b/data/fiirumi_posts.json similarity index 100% rename from fiirumi_posts.json rename to data/fiirumi_posts.json diff --git a/question_posts.json b/data/question_posts.json similarity index 100% rename from question_posts.json rename to data/question_posts.json diff --git a/vaalilakana.json b/data/vaalilakana.json similarity index 100% rename from vaalilakana.json rename to data/vaalilakana.json diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..57bb2e4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +version: '3.4' +services: + bot: + build: . + image: vaalilakanabot:latest + volumes: + - ./data:/bot/data:rw + - ./logs:/bot/logs:rw + env_file: + - bot.env \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..73ce614 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +python-telegram-bot +requests +lxml \ No newline at end of file diff --git a/vaalilakanabot2021.py b/vaalilakanabot2021.py new file mode 100644 index 0000000..071a9d7 --- /dev/null +++ b/vaalilakanabot2021.py @@ -0,0 +1,482 @@ +import os +import re +import time +import json +import requests +import logging + +from telegram.ext import Updater, MessageHandler, CommandHandler, Filters +from lxml import html, etree + +TOKEN = os.environ['VAALILAKANABOT_TOKEN'] +ADMIN_CHAT_ID = os.environ['ADMIN_CHAT_ID'] + +BASE_URL = 'https://fiirumi.fyysikkokilta.fi' +TOPIC_LIST_URL = '{}/c/hottis-fiilaa/l/latest.json'.format(BASE_URL) #TODO: update this to correspond current year discussion board +QUESTION_LIST_URL = '{}/c/kokousreferaatit/l/latest.json'.format(BASE_URL) #TODO: update this to correspond current year discussion board + +channels = [] +vaalilakana = {} +last_applicant = None +fiirumi_posts = [] +question_posts = [] + +logger = logging.getLogger('vaalilakanabot') +logger.setLevel(logging.DEBUG) + +log_path = os.path.join('logs', 'vaalilakanabot.log') +fh = logging.FileHandler(log_path) +fh.setLevel(logging.DEBUG) + +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: {}'.format(vaalilakana)) + +with open('data/channels.json', 'r') as f: + data = f.read() + channels = json.loads(data) + +logger.info('Loaded channels: {}'.format(channels)) + +with open('data/fiirumi_posts.json', 'r') as f: + data = f.read() + fiirumi_posts = json.loads(data) + +logger.info('Loaded fiirumi posts: {}'.format(fiirumi_posts)) + +with open('data/question_posts.json', 'r') as f: + data = f.read() + question_posts = json.loads(data) + +logger.info('Loaded question posts: {}'.format(fiirumi_posts)) + +updater = Updater(TOKEN, use_context=True) + + +def _save_data(filename, data): + with open(filename, 'w') as f: + f.write(json.dumps(data)) + + +def _vaalilakana_to_string(vaalilakana): + output = '' + # Hardcoded to maintain order instead using dict keys + for position in ['Puheenjohtaja', 'Varapuheenjohtaja', 'Rahastonhoitaja', 'Viestintävastaava', + 'IE', 'Hupimestari', 'Yrityssuhdevastaava', 'Kv-vastaava', 'Opintovastaava', 'Fuksikapteeni']: + output += '{position}:\n'.format(position=position) + for applicant in vaalilakana[position]: + link = applicant['fiirumi'] + selected = applicant['valittu'] + if selected == True: + if link: + output += '- {name} (valittu)\n'.format( + name=applicant['name'], + link=link + ) + else: + output += '- {name} (valittu)\n'.format(name=applicant['name']) + else: + if link: + output += '- {name}\n'.format( + name=applicant['name'], + link=link + ) + else: + output += '- {name}\n'.format(name=applicant['name']) + + output += '\n' + return output + + +def _parse_fiirumi_posts(context=updater.bot): + try: + logger.debug(TOPIC_LIST_URL) + 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: + id = topic['id'] + title = topic['title'] + slug = topic['slug'] + if str(id) not in fiirumi_posts: + new_post = { + 'id': id, + 'title': title, + 'slug': slug, + } + fiirumi_posts[str(id)] = new_post + _save_data('data/fiirumi_posts.json', fiirumi_posts) + _announce_to_channels( + 'Uusi postaus Vaalipeli-palstalla!\n{title}\n{base}/t/{slug}/{id}'.format( + title=title, + base=BASE_URL, + slug=slug, + id=id + ) + ) + + for question in question_list: + id = question['id'] + title = question['title'] + slug = question['slug'] + if str(id) not in question_posts: + new_question = { + 'id': id, + 'title': title, + 'slug': slug, + } + question_posts[str(id)] = new_question + _save_data('data/question_posts.json', question_posts) + _announce_to_channels( + 'Uusi kysymys Fiirumilla!\n{title}\n{base}/t/{slug}/{id}'.format( + title=title, + base=BASE_URL, + slug=slug, + id=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: + updater.bot.send_message( + chat_id, + 'Virheelliset parametrit - /poista , ' + ) + raise Exception('Invalid parameters') + + if position not in vaalilakana: + updater.bot.send_message( + chat_id, + 'Tunnistamaton virka: {}'.format(position), + parse_mode='HTML' + ) + raise Exception('Unknown position {}'.format(position)) + + found = None + for applicant in vaalilakana[position]: + if name == applicant['name']: + found = applicant + break + + if not found: + updater.bot.send_message( + chat_id, + 'Hakijaa ei löydy {}'.format(name), + parse_mode='HTML' + ) + raise Exception('Applicant not found: {}'.format(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: + updater.bot.send_message( + chat_id, + 'Virheelliset parametrit - /lisaa_fiirumi , , ' + ) + raise Exception('Invalid parameters') + + if position not in vaalilakana: + updater.bot.send_message( + chat_id, + 'Tunnistamaton virka: {}'.format(position), + parse_mode='HTML' + ) + raise Exception('Unknown position {}'.format(position)) + + if thread_id not in fiirumi_posts: + updater.bot.send_message( + chat_id, + 'Fiirumi-postausta ei löytynyt annetulla id:llä: {}'.format(thread_id), + parse_mode='HTML' + ) + raise Exception('Unknown thread {}'.format(thread_id)) + + found = None + for applicant in vaalilakana[position]: + if name == applicant['name']: + found = applicant + fiirumi = '{base}/t/{slug}/{thread_id}'.format( + base = BASE_URL, + slug = fiirumi_posts[thread_id]['slug'], + thread_id = fiirumi_posts[thread_id]['id']) + applicant['fiirumi'] = fiirumi + break + + if not found: + updater.bot.send_message( + chat_id, + 'Hakijaa ei löydy {}'.format(name), + parse_mode='HTML' + ) + raise Exception('Apllicant not found: {}'.format(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, context): + try: + chat_id = update.message.chat.id + logger.debug(chat_id) + logger.debug(ADMIN_CHAT_ID) + logger.debug(str(chat_id)==str(ADMIN_CHAT_ID)) + if str(chat_id)==str(ADMIN_CHAT_ID): + logger.debug("Oot admin") + text = update.message.text.replace('/lisaa', '').strip() + params = text.split(',') + + try: + position = params[0].strip() + name = params[1].strip() + fiirumi = '' + if len(params) > 2: + thread_id = params[2].strip() + fiirumi = '{base}/t/{slug}/{thread_id}'.format( + base = BASE_URL, + slug = fiirumi_posts[thread_id]['slug'], + thread_id = fiirumi_posts[thread_id]['id']) + except: + updater.bot.send_message( + chat_id, + 'Virheelliset parametrit - /lisaa , , thread ID' + ) + raise Exception('Invalid parameters') + + new_applicant = { + 'name': name, + 'position': position, + 'fiirumi': fiirumi, + 'valittu': False + } + + if position not in vaalilakana: + updater.bot.send_message( + chat_id, + 'Tunnistamaton virka: {}'.format(position), + parse_mode='HTML' + ) + raise Exception('Unknown position {}'.format(position)) + + 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} ({fiirumi}).\n\nLähetä tiedote komennolla /tiedota'.format( + **new_applicant + ), + parse_mode='HTML' + ) + + except Exception as e: + logger.error(e) + +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: + updater.bot.send_message( + chat_id, + 'Virheelliset parametrit - /valittu , ' + ) + raise Exception('Invalid parameters') + + if position not in vaalilakana: + updater.bot.send_message( + chat_id, + 'Tunnistamaton virka: {}'.format(position), + parse_mode='HTML' + ) + raise Exception('Unknown position {}'.format(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, + 'Hakijaa ei löydy {}'.format(name), + parse_mode='HTML' + ) + raise Exception('Apllicant not found: {}'.format(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('New channel added {}'.format(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 jauhis: + updater.bot.send_sticker(chat_id, jauhis) + except Exception as e: + logger.warning("Error in sending Jauhis", e) + + +def error(update, context): + """Log Errors caused by Updates.""" + logger.warning('Update "%s" caused error "%s"', update, context.error) + + + +jq = updater.job_queue +jq.run_repeating(_parse_fiirumi_posts, 60) + +updater.dispatcher.add_handler(CommandHandler('lisaa', add_applicant)) +updater.dispatcher.add_handler(CommandHandler('valittu', add_selected_tag)) +updater.dispatcher.add_handler(CommandHandler('lisaa_fiirumi', add_fiirumi_to_applicant)) +updater.dispatcher.add_handler(CommandHandler('poista', remove_applicant)) +updater.dispatcher.add_handler(CommandHandler('lakana', show_vaalilakana)) +updater.dispatcher.add_handler(CommandHandler('tiedota', announce_new_applicant)) +updater.dispatcher.add_handler(CommandHandler('start', register_channel)) +updater.dispatcher.add_handler(CommandHandler('jauhis', jauhis)) + +updater.dispatcher.add_error_handler(error) +updater.start_polling() +# updater.idle() +