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()
+