taskbot/app/text_commands.py

301 lines
8.8 KiB
Python

"""Collection of all text commands (commands that starts with / ).
This module is the only message interface: other modules should not communicate with the telegram user.
Decorate a function with @command for marking it as a text command.
Functions here should be only for user interaction (messages etc).
"""
from pprint import pprint
from sqlite3 import Error as DBError
from typing import Union, Optional
from functools import wraps
from textwrap import dedent
from telegram import Update, Message, MessageEntity, User, Chat, ChatMember
from telegram.ext import CallbackContext
import helpers as h
# flags and conf
# TODO make a conf file
DEVELOPMENT = True
# decorator functions
__command_dict__ = dict()
def command(func, command_name: str = ""):
"""functions decorated will be counted as '/' commands"""
if not command_name:
command_name = func.__name__
global __command_dict__
__command_dict__[command_name] = func
#@wraps(func)
#def inner(func):
# return func
return func
def get_commands() -> dict:
return __command_dict__
def nullfunc(*, phony):
return
def admin_only(func): # decorator
"""decorator: the command is only to be used by admins. it implies it is only in groups"""
@wraps(func)
def inner(update: Update, context: CallbackContext) -> None:
user: User = update.effective_user
chat: Chat = update.effective_chat
if h.is_admin(user, chat) and h.is_group(chat):
return func
else:
return nullfunc
return inner
def group_only(func): # decorator
"""decorator: the command is only to be used in groups"""
@wraps(func)
def inner(update: Update, context: CallbackContext) -> None:
if h.is_group(update.effective_chat):
return func
else:
return nullfunc
return inner
def private_only(func): # decorator
"""decorator: the command is only to be used in private chats"""
@wraps(func)
def inner(update: Update, context: CallbackContext) -> None:
if h.is_private(update.effective_chat):
return func
else:
return nullfunc
return inner
def dev_only(func): # decorator
"""decorator: the command will be available only durong development"""
if DEVELOPMENT:
return func
else:
return nullfunc
# common strings
reply_text_to_not_admins = "This command is for administrators only. You're not an administrator. Nothing done."
# commands
# def insert_task_command_wrapper(message: Message, command_name: str) -> None:
# """creates the reply text for the insertion command"""
# task_type: str = command_name
# try: #TODO logging
# h.insert_task(message)
# except ValueError: #not a group
# reply_text = "This command must be used only in group chats. Nothing done."
# except DBError: # db error
# reply_text = "Database error, please retry."
# except: # other
# reply_text = "Unknown error, please retry."
# else: # success
# reply_text = f"{task_type.title()} task inserted." #TODO private message to users?
# #TODO user mentions?
@command
def delete(update: Update, context: CallbackContext) -> None:
"""mark as deleted the task in the message replied_to"""
# TODO add syntax for deletion comment (i.e. "/delete because reasons")
# TODO add syntax for deletion of task number without responding (and comment, like "/delete 56 because reasons")
success = True
deleted_task_id = 0
message = update.message
if h.is_from_admin(message):
original_message: Message = message.reply_to_message
deleted_task_id = h.delete_task(deleting_message=message, original_message=original_message)
else:
success = False
update.message.reply_text(f"Task {deleted_task_id} deleted" if success else "Task not deleted")
return
@command
def start(update: Update, context: CallbackContext) -> None:
context.bot.send_message(chat_id=update.effective_chat.id, text="I'm a bot, please talk to me!")
@command
def test(update: Update, context: CallbackContext) -> None:
#update.message.reply_text("ok")
reply = ""
ent = None
try :
ent = h.classify_entities(update.message)
reply += str(ent)
except Exception as e1:
reply += str(e1)
try:
with open("app/.tmp/update", "tw") as f:
f.write(str(update))
except Exception as e2:
reply += str(e2)
print(reply or "None")
update.message.reply_text(reply or "None")
@command
@admin_only
@dev_only
@private_only
def dump(update: Update, context: CallbackContext) -> None:
"""dumps db"""
msg_text = update.message.text
dbs_to_dump = []
if "task" in msg_text:
dbs_to_dump.append("task")
if "people" in msg_text:
dbs_to_dump.append("people")
reply_text = h.dump_db(dbs_to_dump)
update.message.reply_text(reply_text)
@command
def admins(update: Update, context: CallbackContext) -> None:
admins: list[ChatMember] = update.message.chat.get_administrators()
admins_names = [admin.user.username or admin.user.firstname for admin in admins]
reply_text = "Admins: \n" + ", ".join("@"+name for name in admins_names)
update.message.reply_text(reply_text)
@command
def register_person(update: Update, context: CallbackContext) -> None:
"""associate a person id with the telegram id.
multiple telegram accounts for a single person are allowed,
while multiple people with a single shared account is not supported.
admin only"""
#TODO
reply_text = reply_text_to_not_admins or "Not implemented. Nothing done."
update.message.reply_text(reply_text)
raise NotImplementedError
@command
def help(update: Update, context: CallbackContext) -> None:
usage = """Usage: use a command of (/use, /cleaning, /construction, /assistance, /meeting) for inserting a task.
The command must be followed by @mentions of all the participants.
Except for that rule, you can write what you want in the message."""
if "admin" in update.message.text:
usage += """
Admin-only commands:
/register_group name: register a group chat as the official work chat for the place.
/delete: respond with that command to a task for deleting it"""
if "dev" in update.message.text:
usage += """
Dev-only usage:
/chat_number
/test"""
update.message.reply_text(dedent(usage))
## group task commands
@command
def register_task(update: Update, context: CallbackContext, task_type: str) -> None:
message = update.message
mentions = h.get_mentions(message)
participants = h.get_participants(h.classify_entities(mentions))
print(f"{participants=}")
thanks = "\nThanks to " + ", ".join(user.mention_html() for user in participants)
if participants:
task_id = h.insert_task(update)
success_text = f"OK, {task_type} task n.{task_id} registered."
reply_text = success_text + thanks
else:
reply_text = "Task not inserted. Nothing Done."
update.message.reply_html(reply_text)
@command
def assistance(update: Update, context: CallbackContext) -> None:
register_task(update, context, "assistance")
@command
def construction(update: Update, context: CallbackContext) -> None:
register_task(update, context, "construction")
@command
def use(update: Update, context: CallbackContext) -> None:
register_task(update, context, "use")
@command
def cleaning(update: Update, context: CallbackContext) -> None:
register_task(update, context, "cleaning")
@command #dev
def chat_number(update: Update, context: CallbackContext) -> None:
chat_id = update.effective_chat.id
context.bot.send_message(chat_id=chat_id, text=f"{chat_id}")
@command
def register_group(update: Update, context: CallbackContext) -> None:
"""Admin can associate a group to a place name.
Substituting the precedent group:place pair or creating another place.
type this command followed by the name that you want to assign to the place.
Spaces and capitalization are ignored"""
message = update.message
place = h.place_name_from_message(message)
# check if it is a new place, in this case put a wanring #TBD
existing_places = h.list_places()
if place not in existing_places:
warning = f"Warning: the place name you inserted, '{place}', does not exists yet. \
The places currently registered were {existing_places}. \
Inserting it anyway... \n"
success = h.register_group_as_place(message)
if success:
reply_text = f"Current group registered as {place} group "
else:
reply_text = "Not registered. Nothing done"
reply_text = warning + reply_text
message.reply_to_message(reply_text)