From a939485495230673b6000d597746c59006911cbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Ml=C3=A1dek?= Date: Mon, 14 Oct 2019 12:47:51 +0200 Subject: [PATCH] Initial commit. --- .gitignore | 2 + dudlebot.py | 152 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.in | 1 + requirements.txt | 15 +++++ 4 files changed, 170 insertions(+) create mode 100644 .gitignore create mode 100644 dudlebot.py create mode 100644 requirements.in create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e920dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dudlebot.ini +dudlebot.pickle diff --git a/dudlebot.py b/dudlebot.py new file mode 100644 index 0000000..1ac545c --- /dev/null +++ b/dudlebot.py @@ -0,0 +1,152 @@ +import configparser +import logging +import re +from dataclasses import dataclass +from datetime import datetime, timedelta +from enum import Enum +from typing import List, Dict + +import telegram +from telegram.ext import Updater, CommandHandler, PicklePersistence + + +class DudleBot: + def __init__(self, token): + self.logger = logging.getLogger("dudle") + + persistence = PicklePersistence(filename='dudlebot.pickle') + + self.updater = Updater(token, persistence=persistence, use_context=True) + dispatcher = self.updater.dispatcher + + dispatcher.add_handler(CommandHandler('start', self.tg_start)) + dispatcher.add_handler(CommandHandler('newplan', self.tg_newplan)) + dispatcher.add_handler(CommandHandler('plan', self.tg_plan)) + + def tg_start(self, update, _): + update.message.reply_text("Hello! May luck be with you and your plans!") + + def tg_newplan(self, update, context): + periods = { + 'thisweek': datetime.today() - timedelta(days=datetime.today().weekday() % 6), + 'nextweek': datetime.today() + timedelta(days=7) - timedelta(days=datetime.today().weekday() % 6) + } + + period = update.message.text.partition(' ')[2] + if not period: + period = 'nextweek' + + if period not in periods: + update.message.reply_text("Sorry man, I just don't understand.") + return + + context.chat_data['plan'] = Plan(start=periods[period], + duration=timedelta(days=7), # TODO + entries={}) + + self._reply_with_plan(update, context) + + def tg_plan(self, update, context): + if not context.chat_data.get("plan", None): + update.message.reply_text("No plan created yet! (Speak /newplan to do so.)") + return + + responses_str = update.message.text[len("/plan"):] + responses_str = re.sub(r'[^A-Za-z?]', '', responses_str) + responses_str = [char.upper() for char in responses_str] + + if len(responses_str) == 0: + update.message.reply_text("Did you forget something? (Ex. usage: /plan Y N ? Y N Y Y)") + return + + responses_str_padded = [responses_str[i] if i < len(responses_str) else "?" + for i in range(context.chat_data['plan'].duration.days)] + + responses = [] + for char in responses_str_padded: + if char == "Y": + responses.append(PlanResponse.YES) + elif char == "N": + responses.append(PlanResponse.NO) + else: + responses.append(PlanResponse.UNKNOWN) + + user = update.message.from_user + name = user.username or f"{user.first_name} {user.last_name}" + + context.chat_data['plan'].entries[user.id] = PlanEntry(name=name, responses=responses) + + self._reply_with_plan(update, context) + + @staticmethod + def _reply_with_plan(update, context): + plan: Plan = context.chat_data['plan'] + + formatted_plan = f"Poll: {plan.start.strftime('%d.%m.%Y')} -> " \ + f"{(plan.start + plan.duration).strftime('%d.%m.%Y')}" + formatted_plan += "\n\n" + formatted_plan += "```\n" + + days = [plan.start + timedelta(days=i) for i in range(plan.duration.days)] + formatted_plan += f"{'|'.join([day.strftime('%A')[:2] for day in days])}" + formatted_plan += "\n" + + if len(plan.entries) == 0: + entries = {-1: PlanEntry(name="???", responses=[])} + else: + entries = plan.entries + + for entry in entries.values(): + responses = [entry.responses[i] if i < len(entry.responses) else PlanResponse.UNKNOWN + for i in range(plan.duration.days)] + + for response in responses: + formatted_plan += f"{str(response)} " + + formatted_plan += f" {entry.name}" + formatted_plan += "\n" + + formatted_plan += "```" + + update.message.reply_text(formatted_plan, parse_mode=telegram.ParseMode.MARKDOWN) + + def start(self): + self.logger.info("Starting DudleBot...") + self.updater.start_polling() + self.updater.idle() + + +class PlanResponse(Enum): + YES = 1 + NO = 2 + UNKNOWN = 3 + + def __str__(self): + return { + self.YES: "Y", + self.NO: "N", + self.UNKNOWN: "?" + }[self] + + +@dataclass +class PlanEntry: + name: str + responses: List[PlanResponse] + + +@dataclass +class Plan: + start: datetime + duration: timedelta + entries: Dict[int, PlanEntry] + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') + + config = configparser.ConfigParser() + config.read("dudlebot.ini") + + dudlebot = DudleBot(config.get("general", "token")) + dudlebot.start() diff --git a/requirements.in b/requirements.in new file mode 100644 index 0000000..11a50f9 --- /dev/null +++ b/requirements.in @@ -0,0 +1 @@ +python-telegram-bot \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4111c59 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile +# +asn1crypto==1.1.0 # via cryptography +certifi==2019.9.11 # via python-telegram-bot +cffi==1.12.3 # via cryptography +cryptography==2.7 # via python-telegram-bot +future==0.18.0 # via python-telegram-bot +pycparser==2.19 # via cffi +python-telegram-bot==12.1.1 +six==1.12.0 # via cryptography +tornado==6.0.3 # via python-telegram-bot