diff options
author | Mikhail Kirillov <w96k@runbox.com> | 2024-10-10 04:12:00 +0400 |
---|---|---|
committer | Mikhail Kirillov <w96k@runbox.com> | 2024-10-10 04:12:00 +0400 |
commit | 9be02d9141c14a3e3ca28567dbaff671de83200f (patch) | |
tree | ecc8c2f7023654179d51e2fdee68b1d688c131d8 | |
parent | 662342863e37e8661f818218df7bb72bc18bf4eb (diff) |
Fix #9. Add events commands
-rw-r--r-- | commands/__init__.py | 1 | ||||
-rw-r--r-- | commands/event.py | 110 | ||||
-rw-r--r-- | commands/movie.py | 44 | ||||
-rw-r--r-- | commands/users.py | 38 | ||||
-rw-r--r-- | main.py | 8 | ||||
-rw-r--r-- | predicates.py | 31 | ||||
-rw-r--r-- | strings.py | 31 | ||||
-rw-r--r-- | utils.py | 12 |
8 files changed, 249 insertions, 26 deletions
diff --git a/commands/__init__.py b/commands/__init__.py index 09c0867..2e0e009 100644 --- a/commands/__init__.py +++ b/commands/__init__.py @@ -13,3 +13,4 @@ from .meta import about from .users import set_users, add_users, list_users, remove_users, \ chooser_user, who_is_next from .movie import movie, movies, remove_movies +from .event import create_event, list_events diff --git a/commands/event.py b/commands/event.py new file mode 100644 index 0000000..47c34ea --- /dev/null +++ b/commands/event.py @@ -0,0 +1,110 @@ +# This file is part of python-cinema-club-bot +# contributed in 2024 by Mikhail Kirillov (~w96k) <w96k@runbox.com> + +# To the extent possible under law, the author(s) have dedicated all copyright +# and related and neighboring rights to this software to the public domain +# worldwide. This software is distributed without any warranty. + +# You should have received a copy of the CC0 Public Domain Dedication along +# with this software. If not, see: +# <http://creativecommons.org/publicdomain/zero/1.0/> + +from telegram import Update, error +from telegram.ext import ContextTypes +from datetime import datetime + +from utils import context_init +from predicates import is_past, has_finished_event +from strings import EVENT_ARGS_NOT_PROVIDED, EVENT_INVALID_DATETIME, \ + EVENT_CREATED, EVENT_EDITED, EVENT_CANT_BE_IN_PAST, \ + EVENT_WHERE_NOT_PROVIDED, EVENT_MOVIE_NOT_CHOOSEN, EVENTS_LIST, \ + EVENT_USER_HAD_EVENT, NO_EVENTS + + +async def create_event( + update: Update, + context: ContextTypes.DEFAULT_TYPE +) -> None: + context_init(context) + events = context.chat_data["events"] + movies = context.chat_data["movies"] + + username = update.message.from_user.username + last_movie = movies[-1] if movies != [] else None + + if has_finished_event(context, username): + raise error.TelegramError(EVENT_USER_HAD_EVENT) + + if last_movie is None or last_movie["user"] != username: + raise error.TelegramError(EVENT_MOVIE_NOT_CHOOSEN) + + if context.args == []: + raise error.TelegramError(EVENT_ARGS_NOT_PROVIDED) + + argument_when = context.args[0] + + try: + event_when = datetime.strptime(argument_when, "%d.%m.%Y/%H:%M") + except ValueError: + raise error.TelegramError(EVENT_INVALID_DATETIME) + + if is_past(event_when): + raise error.TelegramError( + EVENT_CANT_BE_IN_PAST.format( + when=event_when, + today=datetime.today(), + ) + ) + + arguments_where = context.args[1:] + + if arguments_where == [] and events == []: + raise error.TelegramError(EVENT_WHERE_NOT_PROVIDED) + + event_where = " ".join(arguments_where) + + if event_where == "": + event_where = events[-1]["where"] + + event_dict = dict( + when=event_when, + where=event_where, + movie=last_movie["title"], + user=last_movie["user"] + ) + + if events == [] or is_past(events[-1]["when"]): + events.append(event_dict) + await update.message.reply_text(EVENT_CREATED.format(when=event_when)) + else: + events[-1] = event_dict + await update.message.reply_text(EVENT_EDITED.format(when=event_when)) + + +async def list_events( + update: Update, + context: ContextTypes.DEFAULT_TYPE +) -> None: + context_init(context) + + events = context.chat_data["events"] + movies = context.chat_data["movies"] + + events_formatted: str = "" + + for event in events: + event_finished = "FINISHED" \ + if is_past(event["when"]) else "PLANNED" + + events_formatted += EVENTS_LIST.format( + movie=event["movie"], + user=event["user"], + when=event["when"], + where=event["where"], + finished=event_finished + ) + + if events_formatted == "": + events_formatted = NO_EVENTS + + await update.message.reply_text(events_formatted) diff --git a/commands/movie.py b/commands/movie.py index b7be8cd..256156f 100644 --- a/commands/movie.py +++ b/commands/movie.py @@ -12,13 +12,12 @@ from telegram import Update, error from telegram.ext import ContextTypes from imdb import Cinemagoer -from pprint import pformat - -from utils import context_init +from utils import context_init, choose_next_user +from predicates import has_finished_event from strings import MOVIE_NOT_PROVIDED, EXPECTED_ONE_MOVIE, \ MOVIE_ANOTHER_USER, FETCHING_MOVIE, FETCHING_ERROR, \ - MOVIE_REMOVE, MOVIE_SET + MOVIE_REMOVE, MOVIE_SET, MOVIES_LIST, NO_MOVIES, ADD_MORE_USERS imdb = Cinemagoer() @@ -30,9 +29,18 @@ async def movie( ) -> None: context_init(context) - chooser = context.chat_data["users"][0] or None + users = context.chat_data["users"] + + if users == []: + raise error.TelegramError(ADD_MORE_USERS) + + chooser = users[0] username = update.message.from_user.username + if has_finished_event(context, chooser): + users = context.chat_data["users"] = choose_next_user(users) + raise error.TelegramError(MOVIE_ANOTHER_USER.format(users[0])) + if "@"+username != chooser: raise error.TelegramError(MOVIE_ANOTHER_USER.format(user=chooser)) @@ -50,12 +58,15 @@ async def movie( movie = imdb.get_movie(movie_id) except: raise error.TelegramError(FETCHING_ERROR) - + movie_dict = dict( title=movie.data.get("title"), id=movie.getID(), user=update.effective_user.username, - poster=movie.data.get("cover url") + poster=movie.data.get("cover url"), + rating=movie.data.get("rating"), + genres=movie.data.get("genres"), + runtime=movie.data.get("runtimes"), ) if len(context.chat_data["movies"]) > 0 and \ @@ -75,13 +86,21 @@ async def movies( ) -> None: context_init(context) - movies = context.chat_data["movies"].copy() + movies = context.chat_data["movies"] + + movies_formatted: str = "" for movie in movies: - if movie["poster"] or None: - del movie["poster"] + movies_formatted += MOVIES_LIST.format( + title=movie["title"], + id=movie["id"], + user=movie["user"] + ) - await update.message.reply_text(pformat(movies)) + if movies_formatted == "": + movies_formatted = NO_MOVIES + + await update.message.reply_text(movies_formatted) async def remove_movies( @@ -100,6 +119,5 @@ async def remove_movies( if movie["id"] == movie_id: context.chat_data["movies"].remove(movie) await update.message.reply_text( - MOVIE_REMOVE.format(title=movie["title"], id=["movie.id"]) + MOVIE_REMOVE.format(title=movie["title"], id=movie["id"]) ) - diff --git a/commands/users.py b/commands/users.py index 497d7dd..9dcc125 100644 --- a/commands/users.py +++ b/commands/users.py @@ -15,9 +15,11 @@ from collections import deque from strings import USER_NOT_PROVIDED, USERS_ADDED, USERS_REMOVED, \ EXPECTED_ONE_USER, USER_SET, USER_ADD, USER_REMOVE, ADD_MORE_USERS, \ - NEXT_MOVIE_USER, USER_NOT_FOUND, USER_CHOOSE -from utils import context_init, create_users_string - + NEXT_MOVIE_USER, USER_NOT_FOUND, USER_CHOOSE, NO_USERS, \ + EVENT_USER_HAD_EVENT +from utils import context_init, create_users_string, normalize_username, \ + choose_next_user +from predicates import has_finished_event async def set_users( update: Update, @@ -32,6 +34,7 @@ async def set_users( await update.message.reply_text(USER_SET) + async def add_users( update: Update, context: ContextTypes.DEFAULT_TYPE @@ -56,21 +59,34 @@ async def list_users( users = context.chat_data["users"] - await update.message.reply_markdown(create_users_string(users)) + if users == []: + await update.message.reply_text(NO_USERS) + else: + if has_finished_event(context, users[0]): + users = context.chat_data["users"] = choose_next_user(users) + + await update.message.reply_markdown(create_users_string(users)) async def who_is_next( update: Update, context: ContextTypes.DEFAULT_TYPE ) -> None: + """ + This commands sets the next chooser if needed and shows current + """ + context_init(context) users = context.chat_data["users"] - if len(users) > 0: - await update.message.reply_text(NEXT_MOVIE_USER.format(user=users[0])) - else: - await update.message.reply_text(ADD_MORE_USERS) + if users == []: + raise error.TelegramError(ADD_MORE_USERS) + + if has_finished_event(context, users[0]): + users = context.chat_data["users"] = choose_next_user(users) + + await update.message.reply_text(NEXT_MOVIE_USER.format(user=users[0])) async def remove_users( @@ -118,6 +134,12 @@ async def chooser_user( context.chat_data["users"] = users + if has_finished_event(context, users[0]): + await update.message.reply_text( + EVENT_USER_HAD_EVENT.format(user=users[0]) + ) + users = context.chat_data["users"] = choose_next_user(users) + await update.message.reply_text(USER_CHOOSE.format(user=users[0])) await update.message.reply_markdown(create_users_string(users)) @@ -35,6 +35,7 @@ async def unknown(update: Update, context: ContextTypes.DEFAULT_TYPE): text=INVALID_COMMAND ) + async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None: logging.error("Exception:", exc_info=context.error) @@ -54,15 +55,20 @@ if __name__ == "__main__": # Meta commands app.add_handler(CommandHandler("about", commands.about)) + # Event commands + app.add_handler(CommandHandler("event", commands.create_event)) + app.add_handler(CommandHandler("events", commands.list_events)) + # Movie commands app.add_handler(CommandHandler("movie", commands.movie)) app.add_handler(CommandHandler("movies", commands.movies)) app.add_handler(CommandHandler("movies_remove", commands.remove_movies)) - + # Users commands app.add_handler(CommandHandler("set", commands.set_users)) app.add_handler(CommandHandler("add", commands.add_users)) app.add_handler(CommandHandler("list", commands.list_users)) + app.add_handler(CommandHandler("users", commands.list_users)) app.add_handler(CommandHandler("remove", commands.remove_users)) app.add_handler(CommandHandler("chooser", commands.chooser_user)) app.add_handler(CommandHandler("next", commands.who_is_next)) diff --git a/predicates.py b/predicates.py new file mode 100644 index 0000000..992972d --- /dev/null +++ b/predicates.py @@ -0,0 +1,31 @@ +# This file is part of python-cinema-club-bot +# contributed in 2024 by Mikhail Kirillov (~w96k) <w96k@runbox.com> + +# To the extent possible under law, the author(s) have dedicated all copyright +# and related and neighboring rights to this software to the public domain +# worldwide. This software is distributed without any warranty. + +# You should have received a copy of the CC0 Public Domain Dedication along +# with this software. If not, see: +# <http://creativecommons.org/publicdomain/zero/1.0/> + +from telegram.ext import ContextTypes +from datetime import datetime + +from utils import normalize_username + + +def is_past(dt: datetime) -> bool: + return dt < datetime.today() + + +def has_finished_event( + context: ContextTypes.DEFAULT_TYPE, + username: str +) -> bool: + events = context.chat_data["events"] + last_event = events[-1] if events != [] else None + + return last_event is not None and \ + last_event["user"] == normalize_username(username) and \ + is_past(last_event["when"]) @@ -12,13 +12,15 @@ INVALID_COMMAND = "Invalid command. Available commands: /add /list /remove /chooser /about" MOVIE_ANOTHER_USER = "Movie should choose another user ({user})" -MOVIE_NOT_PROVIDED = "Movie is not provided" +MOVIE_NOT_PROVIDED = "Movie IMDB id is not provided. Use /movie <imdb_id>" MOVIE_NOT_FOUND = "Movie not found on IMDB" EXPECTED_ONE_MOVIE = "Expected only one movie" FETCHING_MOVIE = "Movie with ID {id} is being fetched: https://imdb.com/title/tt{id}/" FETCHING_ERROR = "Couldn't fetch movie or it is not found. Provide IMDB id, for example: 0133093" MOVIE_REMOVE = "Movie \"{title}\" with id {id} has been removed" MOVIE_SET = "Movie \"{title}\" proposed by {user} is succesfully set as next to watch" +MOVIES_LIST = "\"{title}\" IMDB:{id} by {user} \n" +NO_MOVIES = "No movies" USER_NOT_FOUND = "Provided user ({user}) not found. Check /list" USER_NOT_PROVIDED = "User(s) is not provided" @@ -28,11 +30,34 @@ NEXT_MOVIE_USER = "Next movie choice is up to {user}" USER_ADD = "User {user} has been added" USERS_ADDED = "Users have been added successfully. Use /list to view." - USER_REMOVE = "User {user} has been removed. Use /list to view." USERS_REMOVED = "Users has been removed" - USER_SET = "Users have been set successfully. Use /list to view." USER_CHOOSE = "Next movie should choose: {user}" +NO_USERS = "No user added. You can add users by /add <nickname1> <nickname2>" + +EVENT_INVALID_DATETIME = """ +Can't parse provided datetime. It should comply template like this: +/event 1.08.2024/18:00 <where> +""" +EVENT_CANT_BE_IN_PAST = "Event can't happen in the past. You set {when}, but today is {today}" +EVENT_ARGS_NOT_PROVIDED = """ +Event commands need arguments provided: +/event <when> <where> +/event 1.08.2024/18:00 84 Erevan Str 1st entrance 7 floor 451 apartment +""" +EVENT_WHERE_NOT_PROVIDED = """ +Second <where> argument is not provided and previous event doesn't has it. +/event <when> <where> +/event 1.08.2024/18:00 84 Erevan Str 1st entrance 7 floor 451 apartment +""" + +EVENT_CREATED = "Event at {when} was created" +EVENT_EDITED = "Event at {when} was modified" +EVENTS_LIST = "[{finished}] \"{movie}\" by {user} at {when} in {where}\n" +EVENT_MOVIE_NOT_CHOOSEN = "You should choose a film first using /movie <imdb_id>" +EVENT_MOVIE_NOT_SET = "<None>" +EVENT_USER_HAD_EVENT = "User {user} have already finished event, so he can't choose twice. See /next or /list" +NO_EVENTS = "No events" UNDEFINED_ERROR = "Exception: something unexpected happened. Check the logs." @@ -10,7 +10,7 @@ # <http://creativecommons.org/publicdomain/zero/1.0/> from telegram.ext import ContextTypes - +from collections import deque def context_init(context: ContextTypes.DEFAULT_TYPE): """ @@ -23,6 +23,9 @@ def context_init(context: ContextTypes.DEFAULT_TYPE): if "movies" not in context.chat_data: context.chat_data["movies"]: list[dict] = [] + if "events" not in context.chat_data: + context.chat_data["events"]: list[dict] = [] + return context @@ -32,3 +35,10 @@ def normalize_username(username: str): def create_users_string(users: list[str]) -> str: return "`" + ", ".join(users) + "`" + + +def choose_next_user(users: list[dict]) -> list[dict]: + users = deque(users) + users.rotate(-1) # -1 moves list to left by 1 element + + return list(users) |