From 353b83d415061bff2881cbe324273409740be64c Mon Sep 17 00:00:00 2001 From: clarkzjw Date: Fri, 24 Feb 2023 21:08:59 -0800 Subject: bot: implement delayed checkin, (kind of), some TODO left --- bot.py | 31 ++++++++++++++++++++++++---- callback.py | 54 ++++++++++++++++++++++++++++++++++++++++--------- command.py | 23 ++++++++++++++++++++- config.py | 13 ++++++++++-- dbstore/peewee_store.py | 10 +++++++++ prompt/string.py | 4 ++++ 6 files changed, 119 insertions(+), 16 deletions(-) diff --git a/bot.py b/bot.py index 364a85f..fe45970 100644 --- a/bot.py +++ b/bot.py @@ -11,6 +11,7 @@ from starlette.requests import Request from starlette.responses import PlainTextResponse, Response, JSONResponse from starlette.routing import Route from telegram import Update +from telegram.error import BadRequest from telegram.ext import ( Application, CallbackContext, @@ -33,7 +34,8 @@ from callback import ( callback_skip_location_keyword_search, callback_add_comment, callback_skip_comment, - callback_add_media + callback_add_media, + callback_delayed_checkin ) from command import ( start_command, @@ -44,7 +46,8 @@ from command import ( toggle_visibility_command, callback_toggle_visibility, logout_command, - list_command + list_command, + delayed_checkin_command, ) from config import ( FEDI_LOGIN, @@ -57,7 +60,8 @@ from config import ( BOT_TOKEN, BOT_SCOPE, MAIN_MENU, - WAIT_VISIBILITY + WAIT_VISIBILITY, + DELAYED_CHECKIN, ) from prompt.string import PROMPT_CHOOSE_ACTION @@ -114,10 +118,14 @@ async def process_oauth_login_callback(update: FediLoginCallbackUpdate, context: user.access_key = encrypt(access_token, ENCRYPT_KEY) user.save() - text = "You have successfully logged in to your Mastodon account!" + text = "You have successfully logged in to your Mastodon account!" + try: await context.bot.delete_message(chat_id=user.telegram_user_id, message_id=context.user_data[PROMPT_FEDI_LOGIN]) await context.bot.send_message(chat_id=user.telegram_user_id, text=text) await context.bot.send_message(chat_id=user.telegram_user_id, text=PROMPT_CHOOSE_ACTION, reply_markup=MAIN_MENU) + except BadRequest as e: + if "not found" in str(e.message): + pass async def main() -> None: @@ -176,11 +184,26 @@ async def main() -> None: allow_reentry=True, ) + delayed_checkin_handler = ConversationHandler( + entry_points=[ + CommandHandler("delay", delayed_checkin_command), + ], + states={ + DELAYED_CHECKIN: [ + MessageHandler(filters.TEXT & ~filters.COMMAND, callback_delayed_checkin), + ], + }, + fallbacks=[CommandHandler("cancel", cancel_command)], + per_message=False, + allow_reentry=True, + ) + application.add_handler(CommandHandler("logout", logout_command)) application.add_handler(CommandHandler("list", list_command)) application.add_handler(CommandHandler("Help", help_command)) application.add_handler(TypeHandler(type=FediLoginCallbackUpdate, callback=process_oauth_login_callback)) + application.add_handler(delayed_checkin_handler, 3) application.add_handler(visibility_conversation_handler, 2) application.add_handler(checkin_handler, 1) diff --git a/callback.py b/callback.py index 5fe4593..cec6188 100644 --- a/callback.py +++ b/callback.py @@ -13,6 +13,8 @@ from dbstore.peewee_store import User, db, TOOT_VISIBILITY_PRIVATE, TOOT_VISIBIL import uuid from mastodon import Mastodon from util import decrypt, check_user +from datetime import datetime, timedelta +from config import KEY_IS_SCHEDULED_TOOT def generate_uuid(): @@ -131,9 +133,15 @@ async def callback_location_sharing(update: Update, context: ContextTypes.DEFAUL content_type = "text/markdown" if user["home_instance_type"] == "pleroma" else None + if user["delayed_checkin"] > 0: + scheduled_at = datetime.now() + timedelta(minutes=user["delayed_checkin"]) + else: + scheduled_at = None + status = get_mastodon_client(update.effective_user.id).status_post(content, visibility=user["tool_visibility"], content_type=content_type, + scheduled_at=scheduled_at, media_ids=[]) context.user_data[KEY_TOOT_STATUS_ID] = status["id"] @@ -216,17 +224,32 @@ async def _process_location_selection(context: ContextTypes.DEFAULT_TYPE, user: content_type = "text/markdown" if user["home_instance_type"] == "pleroma" else None + if user["delayed_checkin"] > 0: + scheduled_at = datetime.now() + timedelta(minutes=user["delayed_checkin"]) + else: + scheduled_at = None + status = get_mastodon_client(context.user_data["user_id"]).status_post(content, visibility=user["toot_visibility"], content_type=content_type, + scheduled_at=scheduled_at, media_ids=[]) + if "scheduled_at" in status: + context.user_data[KEY_TOOT_STATUS_ID] = status["id"] + context.user_data[KEY_TOOT_STATUS_CONTENT] = content + context.user_data[KEY_IS_SCHEDULED_TOOT] = True - context.user_data[KEY_TOOT_STATUS_ID] = status["id"] - context.user_data[KEY_TOOT_STATUS_CONTENT] = content + await context.bot.send_message(chat_id=context.user_data.get("chat_id"), + text=f"Selected place: {poi_name}, \nToot scheduled at: {status['scheduled_at']}", + parse_mode=ParseMode.MARKDOWN) + elif "url" in status: + context.user_data[KEY_TOOT_STATUS_ID] = status["id"] + context.user_data[KEY_TOOT_STATUS_CONTENT] = content + context.user_data[KEY_IS_SCHEDULED_TOOT] = False - await context.bot.send_message(chat_id=context.user_data.get("chat_id"), - text=f"Selected place: {poi_name}, \nPosted to Mastodon: {status['url']}", - parse_mode=ParseMode.MARKDOWN) + await context.bot.send_message(chat_id=context.user_data.get("chat_id"), + text=f"Selected place: {poi_name}, \nPosted to Mastodon: {status['url']}", + parse_mode=ParseMode.MARKDOWN) msg = await context.bot.send_message(chat_id=context.user_data.get("chat_id"), text=PROMPT_ADD_COMMENT, @@ -276,10 +299,23 @@ async def callback_add_comment(update: Update, context: ContextTypes.DEFAULT_TYP content_type = "text/markdown" if user["home_instance_type"] == "pleroma" else None comment = update.effective_message.text - get_mastodon_client(update.effective_user.id).status_update(id=context.user_data.get(KEY_TOOT_STATUS_ID), - content_type=content_type, - status=f"{comment}\n\n" + context.user_data.get( - KEY_TOOT_STATUS_CONTENT)) + if context.user_data.get(KEY_IS_SCHEDULED_TOOT): + # TODO: + # Mastodon don't have API to update the content of scheduled toots + # we need to store scheduled toots in memory before posting to the server + pass + # get_mastodon_client(update.effective_user.id).scheduled_status_update( + # id=context.user_data.get(KEY_TOOT_STATUS_ID), + # content_type=content_type, + # status=f"{comment}\n\n" + context.user_data.get( + # KEY_TOOT_STATUS_CONTENT)) + + else: + get_mastodon_client(update.effective_user.id).status_update(id=context.user_data.get(KEY_TOOT_STATUS_ID), + content_type=content_type, + status=f"{comment}\n\n" + context.user_data.get( + KEY_TOOT_STATUS_CONTENT)) + context.user_data[KEY_TOOT_STATUS_CONTENT] = f"{comment} " + context.user_data.get(KEY_TOOT_STATUS_CONTENT) return await _process_comment(context) diff --git a/command.py b/command.py index e92dc1c..5ae99f6 100644 --- a/command.py +++ b/command.py @@ -3,6 +3,7 @@ from telegram.constants import ParseMode from telegram.error import BadRequest from telegram.ext import ContextTypes, ConversationHandler from dbstore.peewee_store import get_user_access_key, get_user_home_instance, delete_user_by_id, update_user_visibility +from dbstore.peewee_store import get_user_by_id, update_delayed_checkin from dbstore.peewee_store import TOOT_VISIBILITY_PRIVATE, TOOT_VISIBILITY_UNLISTED, TOOT_VISIBILITY_PUBLIC from config import * from util import check_user @@ -54,7 +55,27 @@ async def logout_command(update: Update, context: ContextTypes.DEFAULT_TYPE, use await update.message.reply_text(PROMPT_LOGOUT_SUCCESS, parse_mode=ParseMode.HTML, reply_markup=LOGIN_MENU) -@check_user +async def delayed_checkin_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + await update.message.reply_text(PROMPT_DELAYED_CHECKIN, parse_mode=ParseMode.HTML) + return DELAYED_CHECKIN + + +async def callback_delayed_checkin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: + try: + delayed_minutes = int(update.effective_message.text) + except ValueError: + await update.message.edit_text(text="Integer expected, try again") + return DELAYED_CHECKIN + + if delayed_minutes < 5: + delayed_minutes = 0 + + update_delayed_checkin(str(update.effective_user.id), delayed_minutes) + u = get_user_by_id(str(update.effective_user.id)) + await update.message.reply_text(text=f"Delayed check-in set to {u['delayed_checkin']} minutes") + return ConversationHandler.END + + async def toggle_visibility_command(update: Update, context: ContextTypes.DEFAULT_TYPE, user: User) -> int: visibility_menu = InlineKeyboardMarkup([ [InlineKeyboardButton("Private", callback_data=TOOT_VISIBILITY_PRIVATE)], diff --git a/config.py b/config.py index fe69bd9..bff9313 100644 --- a/config.py +++ b/config.py @@ -18,7 +18,11 @@ BOT_PORT = int(config["API"]["BOT_PORT"]) MEDIA_GROUP_TIMEOUT = 3 -FEDI_LOGIN, WAIT_VISIBILITY, WAIT_LOCATION, LOCATION_SEARCH_KEYWORD, LOCATION_CONFIRMATION, ADD_MEDIA, ADD_COMMENT = range(7) +FEDI_LOGIN, \ + WAIT_VISIBILITY, \ + DELAYED_CHECKIN, \ + WAIT_LOCATION, LOCATION_SEARCH_KEYWORD, LOCATION_CONFIRMATION, \ + ADD_MEDIA, ADD_COMMENT = range(8) MAIN_MENU = ReplyKeyboardMarkup([ [KeyboardButton(text="Check-in here", request_location=True)], @@ -46,5 +50,10 @@ class MsgDict(TypedDict): KEY_TOOT_STATUS_ID = "toot_status_id" KEY_TOOT_STATUS_CONTENT = "toot_status_content" +KEY_IS_SCHEDULED_TOOT = "is_scheduled_toot" -BOT_SCOPE = ['read:accounts', 'write:media', 'write:statuses'] +# TODO: +# use the first scope as default +# if user set delayed post, ask user consent and request OAuth token again +# DEFAULT_BOT_SCOPE = ['read:accounts', 'write:media', 'write:statuses'] +BOT_SCOPE = ['read:accounts', 'read:statuses', 'write:media', 'write:statuses'] diff --git a/dbstore/peewee_store.py b/dbstore/peewee_store.py index d05c642..ebf3536 100644 --- a/dbstore/peewee_store.py +++ b/dbstore/peewee_store.py @@ -23,6 +23,15 @@ class User(BaseModel): client_id = CharField(max_length=128) client_secret = CharField(max_length=128) toot_visibility = CharField(max_length=128, default=TOOT_VISIBILITY_PRIVATE) + # delayed checkin in minutes + delayed_checkin = IntegerField(default=0) + + +def update_delayed_checkin(telegram_user_id: str, delayed_checkin: int) -> int: + with db.connection_context(): + return User.update(delayed_checkin=delayed_checkin).where( + User.telegram_user_id == telegram_user_id + ).execute() def update_user_visibility(telegram_user_id: str, visibility: str) -> int: @@ -45,6 +54,7 @@ def get_user_by_id(telegram_user_id: str) -> dict: "client_id": user.client_id, "client_secret": user.client_secret, "toot_visibility": user.toot_visibility, + "delayed_checkin": user.delayed_checkin, } except DoesNotExist: return {} diff --git a/prompt/string.py b/prompt/string.py index 3cc0a24..32f9aee 100644 --- a/prompt/string.py +++ b/prompt/string.py @@ -37,9 +37,13 @@ PROMPT_HELP = "Available commands:" \ "\n`/list` - list current linked Fediverse accounts" \ "\n`/logout` - logout from specified Fediverse account, default logout from all" \ "\n`/vis` - toggle visibility of your checkins on specified instances, default=private" \ + "\n`/delay` - set delayed checkin in minutes, default=0" \ "\n`/tos` - show ToS message" \ "\n`/cancel` - cancel current operation during checkins" +PROMPT_DELAYED_CHECKIN = "How many minutes to delay your checkin? By default, checkin will be posted immediately.\n" \ + "If you wish to set delayed checkin, at least 5 minutes is required. \n" \ + "Input less than 5 will be set to 0 (immediate checkin)." PROMPT_LIST = "You are linked with the following Fediverse accounts:" PROMPT_LIST_NO_RESULT = "You are not linked with any Fediverse account yet. \n\n Input /login to login." PROMPT_LOGOUT = "Choose Fediverse account to logout" -- cgit v1.2.3