Index: commands/__init__.py
===================================================================
--- commands/__init__.py	(revision 662342863e37e8661f818218df7bb72bc18bf4eb)
+++ commands/__init__.py	(revision 9be02d9141c14a3e3ca28567dbaff671de83200f)
@@ -14,2 +14,3 @@
     chooser_user, who_is_next
 from .movie import movie, movies, remove_movies
+from .event import create_event, list_events
Index: commands/event.py
===================================================================
--- commands/event.py	(revision 9be02d9141c14a3e3ca28567dbaff671de83200f)
+++ commands/event.py	(revision 9be02d9141c14a3e3ca28567dbaff671de83200f)
@@ -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)
Index: commands/movie.py
===================================================================
--- commands/movie.py	(revision 662342863e37e8661f818218df7bb72bc18bf4eb)
+++ commands/movie.py	(revision 9be02d9141c14a3e3ca28567dbaff671de83200f)
@@ -13,11 +13,10 @@
 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
 
 
@@ -31,6 +30,15 @@
     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:
@@ -51,10 +59,13 @@
     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"),
     )
 
@@ -76,11 +87,19 @@
     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)
 
 
@@ -101,5 +120,4 @@
                 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"])
                 )
-
Index: commands/users.py
===================================================================
--- commands/users.py	(revision 662342863e37e8661f818218df7bb72bc18bf4eb)
+++ commands/users.py	(revision 9be02d9141c14a3e3ca28567dbaff671de83200f)
@@ -16,7 +16,9 @@
 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(
@@ -32,4 +34,5 @@
 
     await update.message.reply_text(USER_SET)
+
 
 async def add_users(
@@ -57,5 +60,11 @@
     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))
 
 
@@ -64,12 +73,19 @@
         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]))
 
 
@@ -119,4 +135,10 @@
     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]))
 
Index: main.py
===================================================================
--- main.py	(revision 662342863e37e8661f818218df7bb72bc18bf4eb)
+++ main.py	(revision 9be02d9141c14a3e3ca28567dbaff671de83200f)
@@ -36,4 +36,5 @@
     )
 
+
 async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
     logging.error("Exception:", exc_info=context.error)
@@ -55,13 +56,18 @@
     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))
Index: predicates.py
===================================================================
--- predicates.py	(revision 9be02d9141c14a3e3ca28567dbaff671de83200f)
+++ predicates.py	(revision 9be02d9141c14a3e3ca28567dbaff671de83200f)
@@ -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"])
Index: strings.py
===================================================================
--- strings.py	(revision 662342863e37e8661f818218df7bb72bc18bf4eb)
+++ strings.py	(revision 9be02d9141c14a3e3ca28567dbaff671de83200f)
@@ -13,5 +13,5 @@
 
 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"
@@ -20,4 +20,6 @@
 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"
@@ -29,10 +31,33 @@
 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."
Index: utils.py
===================================================================
--- utils.py	(revision 662342863e37e8661f818218df7bb72bc18bf4eb)
+++ utils.py	(revision 9be02d9141c14a3e3ca28567dbaff671de83200f)
@@ -11,5 +11,5 @@
 
 from telegram.ext import ContextTypes
-
+from collections import deque
 
 def context_init(context: ContextTypes.DEFAULT_TYPE):
@@ -24,4 +24,7 @@
         context.chat_data["movies"]: list[dict] = []
 
+    if "events" not in context.chat_data:
+        context.chat_data["events"]: list[dict] = []
+
     return context
 
@@ -33,2 +36,9 @@
 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)
