Repository: tingvarsson/telegram.ongabot

Issue:
Automatically create new event
E.g. on Sunday evening.

Probably need to be activated/deactived via explicit commands rather than kicking off directly on /start.

* Two new handlers to schedule and deschedule weekly poll creation job
    * /schedule - add job to job queue
    * /deschedule - remove job from queue
* Default scheduling to sunday evening at 20:00
    * Maybe add day parameter?  Make scheduled day configurable, e.g. `/schedule monday` to schedule job on mondays.

Discussion:

Patch:
diff --git a/ongabot/handler/__init__.py b/ongabot/handler/__init__.py
index a936fa5..777eb0f 100644
--- a/ongabot/handler/__init__.py
+++ b/ongabot/handler/__init__.py
@@ -7,6 +7,8 @@ from .neweventcommandhandler import NewEventCommandHandler
 from .canceleventcommandhandler import CancelEventCommandHandler
 from .ongacommandhandler import OngaCommandHandler
 from .startcommandhandler import StartCommandHandler
+from .schedulecommandhandler import ScheduleCommandHandler
+from .deschedulecommandhandler import DeScheduleCommandHandler
 
 __all__ = (
     "EventPollAnswerHandler",
@@ -16,4 +18,6 @@ __all__ = (
     "CancelEventCommandHandler",
     "OngaCommandHandler",
     "StartCommandHandler",
+    "ScheduleCommandHandler",
+    "DeScheduleCommandHandler",
 )
diff --git a/ongabot/handler/deschedulecommandhandler.py b/ongabot/handler/deschedulecommandhandler.py
new file mode 100644
index 0000000..19d1e70
--- /dev/null
+++ b/ongabot/handler/deschedulecommandhandler.py
@@ -0,0 +1,34 @@
+"""This module contains the DeScheduleCommandHandler class."""
+import logging
+from telegram import Update
+from telegram.ext import CommandHandler, CallbackContext
+
+from utils.log import log
+
+_logger = logging.getLogger(__name__)
+
+
+class DeScheduleCommandHandler(CommandHandler):
+    """Handler for /schedule command"""
+
+    def __init__(self) -> None:
+        CommandHandler.__init__(self, "deschedule", callback=callback)
+
+
+@log
+def callback(update: Update, context: CallbackContext) -> None:
+    """Cancels existing jobs"""
+    _logger.debug("update:\n%s", update)
+
+    current_jobs = context.job_queue.get_jobs_by_name("Weekly scheduled poll creation job")
+
+    if not current_jobs:
+        update.message.reply_text("No jobs to cancel")
+        return
+
+    for job in current_jobs:
+        job.schedule_removal()
+
+    update.message.reply_text(
+        "Job cancelled successfully. Polls will no longer be automatically created"
+    )
diff --git a/ongabot/handler/neweventcommandhandler.py b/ongabot/handler/neweventcommandhandler.py
index adf1fb8..85f3229 100644
--- a/ongabot/handler/neweventcommandhandler.py
+++ b/ongabot/handler/neweventcommandhandler.py
@@ -31,7 +31,7 @@ def callback(update: Update, context: CallbackContext) -> None:
     pinned_poll = context.chat_data.get("pinned_poll_msg")
 
     if pinned_poll is not None:
-        next_wed = helper.get_upcoming_wednesday_date(date.today()).strftime("%Y-%m-%d")
+        next_wed = helper.get_upcoming_date(date.today(), "wednesday").strftime("%Y-%m-%d")
         if next_wed in pinned_poll.poll.question:
             context.bot.send_message(
                 update.effective_chat.id,
@@ -73,7 +73,7 @@ def callback(update: Update, context: CallbackContext) -> None:
 def create_poll_text() -> str:
     """Create text field for poll"""
     title = "Event: ONGA"
-    when = f"When: {helper.get_upcoming_wednesday_date(date.today())}"
+    when = f"When: {helper.get_upcoming_date(date.today(), 'wednesday')}"
     text = f"{title}\n{when}"
     return text
 
diff --git a/ongabot/handler/schedulecommandhandler.py b/ongabot/handler/schedulecommandhandler.py
new file mode 100644
index 0000000..9a6d2e8
--- /dev/null
+++ b/ongabot/handler/schedulecommandhandler.py
@@ -0,0 +1,107 @@
+"""This module contains the ScheduleCommandHandler class."""
+import logging
+import typing
+from datetime import date, datetime, timedelta
+from typing import Any
+
+from telegram import Update
+from telegram.ext import CommandHandler, CallbackContext
+
+from utils import helper
+from utils.log import log
+from .neweventcommandhandler import callback as create_new_poll
+
+
+_logger = logging.getLogger(__name__)
+
+
+class ScheduleCommandHandler(CommandHandler):
+    """Handler for /schedule command"""
+
+    def __init__(self) -> None:
+        CommandHandler.__init__(self, "schedule", callback=callback)
+
+
+@log
+def callback(update: Update, context: CallbackContext) -> None:
+    """Schedule a poll creation job to run every week"""
+    job_name = "Weekly scheduled poll creation job"
+    day_to_schedule = "sunday"
+    _logger.debug("update:\n%s", update)
+
+    if check_if_job_exists(job_name, context):
+        update.message.reply_text(
+            "Scheduled job already exists. "
+            "Send /deschedule first if you wish to create a new scheduled job"
+        )
+        return
+
+    if len(context.args) > 1:
+        update.message.reply_text(
+            "Only one argument supported `/schedule <day>[OPTIONAL]`\n\n"
+            "Example:\n`/schedule` default to schedule job on sundays \n"
+            "`/schedule monday` to schedule job on mondays",
+            parse_mode="MarkdownV2",
+        )
+        return
+
+    if len(context.args) == 1:
+        if not is_args_valid(context.args[0]):
+            update.message.reply_text("Please provide a day that I understand. Bitch.")
+            return
+        day_to_schedule = context.args[0]
+
+    poll_creation_dto = {"context": context, "update": update}
+
+    upcoming_scheduled_date = helper.get_upcoming_date(date.today(), day_to_schedule)
+
+    job = context.job_queue.run_repeating(
+        create_poll,
+        interval=timedelta(weeks=1),
+        first=datetime(
+            upcoming_scheduled_date.year,
+            upcoming_scheduled_date.month,
+            upcoming_scheduled_date.day,
+            20,
+            0,
+            0,
+        ),
+        context=poll_creation_dto,
+        name=job_name,
+    )
+    update.message.reply_text(
+        f"Poll creation is now scheduled to run every {day_to_schedule} "
+        f"starting on {job.next_t:%Y-%m-%d %H:%M} ({job.next_t.tzinfo})"
+    )
+
+
+def check_if_job_exists(name: str, context: CallbackContext) -> bool:
+    """Returns true of false whether job already exists."""
+    current_jobs = context.job_queue.get_jobs_by_name(name)
+    if not current_jobs:
+        return False
+    return True
+
+
+def is_args_valid(day: str) -> bool:
+    """Validates that the supplied arg is a valid day"""
+    if day.lower() not in [
+        "monday",
+        "tuesday",
+        "wednesday",
+        "thursday",
+        "friday",
+        "saturday",
+        "sunday",
+    ]:
+        return False
+    return True
+
+
+def create_poll(context: CallbackContext) -> None:
+    """Creates a new poll by calling /neweventcommand"""
+    _logger.debug("Poll creation is triggered by timer on %s", datetime.now())
+    create_new_poll(
+        typing.cast(typing.Dict[str, Any], context.job.context)["update"],
+        typing.cast(typing.Dict[str, Any], context.job.context)["context"],
+    )
diff --git a/ongabot/ongabot.py b/ongabot/ongabot.py
index a03eefd..640bca5 100755
--- a/ongabot/ongabot.py
+++ b/ongabot/ongabot.py
@@ -13,6 +13,8 @@ from handler import NewEventCommandHandler
 from handler import CancelEventCommandHandler
 from handler import OngaCommandHandler
 from handler import StartCommandHandler
+from handler import ScheduleCommandHandler
+from handler import DeScheduleCommandHandler
 
 
 logging.basicConfig(
@@ -42,6 +44,8 @@ def main() -> None:
     dispatcher.add_handler(CancelEventCommandHandler())
     dispatcher.add_handler(EventPollHandler())
     dispatcher.add_handler(EventPollAnswerHandler())
+    dispatcher.add_handler(ScheduleCommandHandler())
+    dispatcher.add_handler(DeScheduleCommandHandler())
     dispatcher.add_error_handler(error)
 
     # Start the bot
diff --git a/ongabot/utils/helper.py b/ongabot/utils/helper.py
index c411712..57d2328 100644
--- a/ongabot/utils/helper.py
+++ b/ongabot/utils/helper.py
@@ -19,14 +19,31 @@ def create_help_text() -> str:
         "/onga - This is the way, let me show you\n"
         "/newevent - Create a new event corresponding to upcoming Wednesday and pins it\n"
         "/cancelevent - Cancels the latest default event, i.e. unpins it\n"
+        "/schedule <day>[OPTIONAL] - Schedules a job to create a poll on "
+        "every upcoming <day>. If no argument is supplied, job"
+        " is scheduled to run on sundays\n"
+        "/deschedule - Deschedules jobs"
     )
 
     return text
 
 
-def get_upcoming_wednesday_date(today: date) -> date:
-    """Get the date of the next upcoming wednesday"""
-    wednesday_day_of_week_index = 2  # 0-6, 0 is monday and 6 is sunday
-    next_wednesday_date = today + timedelta((wednesday_day_of_week_index - today.weekday()) % 7)
+def get_upcoming_date(today: date, upcoming_weekday: str) -> date:
+    """Get the date of the next upcoming day with name upcoming_weekday"""
+    index = get_weekday_index_from_name(upcoming_weekday)
 
-    return next_wednesday_date
+    next_date = today + timedelta((index - today.weekday()) % 7)
+    return next_date
+
+
+def get_weekday_index_from_name(day_name: str) -> int:
+    """Get the day index from week day name"""
+    return {
+        "monday": 0,
+        "tuesday": 1,
+        "wednesday": 2,
+        "thursday": 3,
+        "friday": 4,
+        "saturday": 5,
+        "sunday": 6,
+    }[day_name.lower()]


Test patch:
diff --git a/tests/test_helper.py b/tests/test_helper.py
index 8d80e83..cf202ea 100644
--- a/tests/test_helper.py
+++ b/tests/test_helper.py
@@ -15,7 +15,7 @@ class HelperTest(unittest.TestCase):
         ]
     )
     def test_getUpcomingWednesdayDate(self, _, today, expected):
-        result = helper.get_upcoming_wednesday_date(today)
+        result = helper.get_upcoming_date(today, "wednesday")
         self.assertEqual(
             result.weekday(),
             2,