diff options
author | clarkzjw <[email protected]> | 2023-02-24 21:08:59 -0800 |
---|---|---|
committer | clarkzjw <[email protected]> | 2023-02-28 15:58:02 -0800 |
commit | 353b83d415061bff2881cbe324273409740be64c (patch) | |
tree | 420fa402e7eda3425fd69b24959cffb0b2cc5039 | |
parent | 0041eb4f9893687565e444be9d50648f49aa4d91 (diff) | |
download | swarm2fediverse-feature/delayed_checkin.tar.gz |
bot: implement delayed checkin, (kind of), some TODO leftfeature/delayed_checkin
-rw-r--r-- | bot.py | 31 | ||||
-rw-r--r-- | callback.py | 54 | ||||
-rw-r--r-- | command.py | 23 | ||||
-rw-r--r-- | config.py | 13 | ||||
-rw-r--r-- | dbstore/peewee_store.py | 10 | ||||
-rw-r--r-- | prompt/string.py | 4 |
6 files changed, 119 insertions, 16 deletions
@@ -11,6 +11,7 @@ from starlette.requests import Request | |||
11 | from starlette.responses import PlainTextResponse, Response, JSONResponse | 11 | from starlette.responses import PlainTextResponse, Response, JSONResponse |
12 | from starlette.routing import Route | 12 | from starlette.routing import Route |
13 | from telegram import Update | 13 | from telegram import Update |
14 | from telegram.error import BadRequest | ||
14 | from telegram.ext import ( | 15 | from telegram.ext import ( |
15 | Application, | 16 | Application, |
16 | CallbackContext, | 17 | CallbackContext, |
@@ -33,7 +34,8 @@ from callback import ( | |||
33 | callback_skip_location_keyword_search, | 34 | callback_skip_location_keyword_search, |
34 | callback_add_comment, | 35 | callback_add_comment, |
35 | callback_skip_comment, | 36 | callback_skip_comment, |
36 | callback_add_media | 37 | callback_add_media, |
38 | callback_delayed_checkin | ||
37 | ) | 39 | ) |
38 | from command import ( | 40 | from command import ( |
39 | start_command, | 41 | start_command, |
@@ -44,7 +46,8 @@ from command import ( | |||
44 | toggle_visibility_command, | 46 | toggle_visibility_command, |
45 | callback_toggle_visibility, | 47 | callback_toggle_visibility, |
46 | logout_command, | 48 | logout_command, |
47 | list_command | 49 | list_command, |
50 | delayed_checkin_command, | ||
48 | ) | 51 | ) |
49 | from config import ( | 52 | from config import ( |
50 | FEDI_LOGIN, | 53 | FEDI_LOGIN, |
@@ -57,7 +60,8 @@ from config import ( | |||
57 | BOT_TOKEN, | 60 | BOT_TOKEN, |
58 | BOT_SCOPE, | 61 | BOT_SCOPE, |
59 | MAIN_MENU, | 62 | MAIN_MENU, |
60 | WAIT_VISIBILITY | 63 | WAIT_VISIBILITY, |
64 | DELAYED_CHECKIN, | ||
61 | ) | 65 | ) |
62 | 66 | ||
63 | from prompt.string import PROMPT_CHOOSE_ACTION | 67 | from prompt.string import PROMPT_CHOOSE_ACTION |
@@ -114,10 +118,14 @@ async def process_oauth_login_callback(update: FediLoginCallbackUpdate, context: | |||
114 | user.access_key = encrypt(access_token, ENCRYPT_KEY) | 118 | user.access_key = encrypt(access_token, ENCRYPT_KEY) |
115 | user.save() | 119 | user.save() |
116 | 120 | ||
117 | text = "You have successfully logged in to your Mastodon account!" | 121 | text = "You have successfully logged in to your Mastodon account!" |
122 | try: | ||
118 | await context.bot.delete_message(chat_id=user.telegram_user_id, message_id=context.user_data[PROMPT_FEDI_LOGIN]) | 123 | await context.bot.delete_message(chat_id=user.telegram_user_id, message_id=context.user_data[PROMPT_FEDI_LOGIN]) |
119 | await context.bot.send_message(chat_id=user.telegram_user_id, text=text) | 124 | await context.bot.send_message(chat_id=user.telegram_user_id, text=text) |
120 | await context.bot.send_message(chat_id=user.telegram_user_id, text=PROMPT_CHOOSE_ACTION, reply_markup=MAIN_MENU) | 125 | await context.bot.send_message(chat_id=user.telegram_user_id, text=PROMPT_CHOOSE_ACTION, reply_markup=MAIN_MENU) |
126 | except BadRequest as e: | ||
127 | if "not found" in str(e.message): | ||
128 | pass | ||
121 | 129 | ||
122 | 130 | ||
123 | async def main() -> None: | 131 | async def main() -> None: |
@@ -176,11 +184,26 @@ async def main() -> None: | |||
176 | allow_reentry=True, | 184 | allow_reentry=True, |
177 | ) | 185 | ) |
178 | 186 | ||
187 | delayed_checkin_handler = ConversationHandler( | ||
188 | entry_points=[ | ||
189 | CommandHandler("delay", delayed_checkin_command), | ||
190 | ], | ||
191 | states={ | ||
192 | DELAYED_CHECKIN: [ | ||
193 | MessageHandler(filters.TEXT & ~filters.COMMAND, callback_delayed_checkin), | ||
194 | ], | ||
195 | }, | ||
196 | fallbacks=[CommandHandler("cancel", cancel_command)], | ||
197 | per_message=False, | ||
198 | allow_reentry=True, | ||
199 | ) | ||
200 | |||
179 | application.add_handler(CommandHandler("logout", logout_command)) | 201 | application.add_handler(CommandHandler("logout", logout_command)) |
180 | application.add_handler(CommandHandler("list", list_command)) | 202 | application.add_handler(CommandHandler("list", list_command)) |
181 | application.add_handler(CommandHandler("Help", help_command)) | 203 | application.add_handler(CommandHandler("Help", help_command)) |
182 | application.add_handler(TypeHandler(type=FediLoginCallbackUpdate, callback=process_oauth_login_callback)) | 204 | application.add_handler(TypeHandler(type=FediLoginCallbackUpdate, callback=process_oauth_login_callback)) |
183 | 205 | ||
206 | application.add_handler(delayed_checkin_handler, 3) | ||
184 | application.add_handler(visibility_conversation_handler, 2) | 207 | application.add_handler(visibility_conversation_handler, 2) |
185 | application.add_handler(checkin_handler, 1) | 208 | application.add_handler(checkin_handler, 1) |
186 | 209 | ||
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 | |||
13 | import uuid | 13 | import uuid |
14 | from mastodon import Mastodon | 14 | from mastodon import Mastodon |
15 | from util import decrypt, check_user | 15 | from util import decrypt, check_user |
16 | from datetime import datetime, timedelta | ||
17 | from config import KEY_IS_SCHEDULED_TOOT | ||
16 | 18 | ||
17 | 19 | ||
18 | def generate_uuid(): | 20 | def generate_uuid(): |
@@ -131,9 +133,15 @@ async def callback_location_sharing(update: Update, context: ContextTypes.DEFAUL | |||
131 | 133 | ||
132 | content_type = "text/markdown" if user["home_instance_type"] == "pleroma" else None | 134 | content_type = "text/markdown" if user["home_instance_type"] == "pleroma" else None |
133 | 135 | ||
136 | if user["delayed_checkin"] > 0: | ||
137 | scheduled_at = datetime.now() + timedelta(minutes=user["delayed_checkin"]) | ||
138 | else: | ||
139 | scheduled_at = None | ||
140 | |||
134 | status = get_mastodon_client(update.effective_user.id).status_post(content, | 141 | status = get_mastodon_client(update.effective_user.id).status_post(content, |
135 | visibility=user["tool_visibility"], | 142 | visibility=user["tool_visibility"], |
136 | content_type=content_type, | 143 | content_type=content_type, |
144 | scheduled_at=scheduled_at, | ||
137 | media_ids=[]) | 145 | media_ids=[]) |
138 | 146 | ||
139 | context.user_data[KEY_TOOT_STATUS_ID] = status["id"] | 147 | context.user_data[KEY_TOOT_STATUS_ID] = status["id"] |
@@ -216,17 +224,32 @@ async def _process_location_selection(context: ContextTypes.DEFAULT_TYPE, user: | |||
216 | 224 | ||
217 | content_type = "text/markdown" if user["home_instance_type"] == "pleroma" else None | 225 | content_type = "text/markdown" if user["home_instance_type"] == "pleroma" else None |
218 | 226 | ||
227 | if user["delayed_checkin"] > 0: | ||
228 | scheduled_at = datetime.now() + timedelta(minutes=user["delayed_checkin"]) | ||
229 | else: | ||
230 | scheduled_at = None | ||
231 | |||
219 | status = get_mastodon_client(context.user_data["user_id"]).status_post(content, | 232 | status = get_mastodon_client(context.user_data["user_id"]).status_post(content, |
220 | visibility=user["toot_visibility"], | 233 | visibility=user["toot_visibility"], |
221 | content_type=content_type, | 234 | content_type=content_type, |
235 | scheduled_at=scheduled_at, | ||
222 | media_ids=[]) | 236 | media_ids=[]) |
237 | if "scheduled_at" in status: | ||
238 | context.user_data[KEY_TOOT_STATUS_ID] = status["id"] | ||
239 | context.user_data[KEY_TOOT_STATUS_CONTENT] = content | ||
240 | context.user_data[KEY_IS_SCHEDULED_TOOT] = True | ||
223 | 241 | ||
224 | context.user_data[KEY_TOOT_STATUS_ID] = status["id"] | 242 | await context.bot.send_message(chat_id=context.user_data.get("chat_id"), |
225 | context.user_data[KEY_TOOT_STATUS_CONTENT] = content | 243 | text=f"Selected place: {poi_name}, \nToot scheduled at: {status['scheduled_at']}", |
244 | parse_mode=ParseMode.MARKDOWN) | ||
245 | elif "url" in status: | ||
246 | context.user_data[KEY_TOOT_STATUS_ID] = status["id"] | ||
247 | context.user_data[KEY_TOOT_STATUS_CONTENT] = content | ||
248 | context.user_data[KEY_IS_SCHEDULED_TOOT] = False | ||
226 | 249 | ||
227 | await context.bot.send_message(chat_id=context.user_data.get("chat_id"), | 250 | await context.bot.send_message(chat_id=context.user_data.get("chat_id"), |
228 | text=f"Selected place: {poi_name}, \nPosted to Mastodon: {status['url']}", | 251 | text=f"Selected place: {poi_name}, \nPosted to Mastodon: {status['url']}", |
229 | parse_mode=ParseMode.MARKDOWN) | 252 | parse_mode=ParseMode.MARKDOWN) |
230 | 253 | ||
231 | msg = await context.bot.send_message(chat_id=context.user_data.get("chat_id"), | 254 | msg = await context.bot.send_message(chat_id=context.user_data.get("chat_id"), |
232 | text=PROMPT_ADD_COMMENT, | 255 | text=PROMPT_ADD_COMMENT, |
@@ -276,10 +299,23 @@ async def callback_add_comment(update: Update, context: ContextTypes.DEFAULT_TYP | |||
276 | content_type = "text/markdown" if user["home_instance_type"] == "pleroma" else None | 299 | content_type = "text/markdown" if user["home_instance_type"] == "pleroma" else None |
277 | 300 | ||
278 | comment = update.effective_message.text | 301 | comment = update.effective_message.text |
279 | get_mastodon_client(update.effective_user.id).status_update(id=context.user_data.get(KEY_TOOT_STATUS_ID), | 302 | if context.user_data.get(KEY_IS_SCHEDULED_TOOT): |
280 | content_type=content_type, | 303 | # TODO: |
281 | status=f"{comment}\n\n" + context.user_data.get( | 304 | # Mastodon don't have API to update the content of scheduled toots |
282 | KEY_TOOT_STATUS_CONTENT)) | 305 | # we need to store scheduled toots in memory before posting to the server |
306 | pass | ||
307 | # get_mastodon_client(update.effective_user.id).scheduled_status_update( | ||
308 | # id=context.user_data.get(KEY_TOOT_STATUS_ID), | ||
309 | # content_type=content_type, | ||
310 | # status=f"{comment}\n\n" + context.user_data.get( | ||
311 | # KEY_TOOT_STATUS_CONTENT)) | ||
312 | |||
313 | else: | ||
314 | get_mastodon_client(update.effective_user.id).status_update(id=context.user_data.get(KEY_TOOT_STATUS_ID), | ||
315 | content_type=content_type, | ||
316 | status=f"{comment}\n\n" + context.user_data.get( | ||
317 | KEY_TOOT_STATUS_CONTENT)) | ||
318 | |||
283 | context.user_data[KEY_TOOT_STATUS_CONTENT] = f"{comment} " + context.user_data.get(KEY_TOOT_STATUS_CONTENT) | 319 | context.user_data[KEY_TOOT_STATUS_CONTENT] = f"{comment} " + context.user_data.get(KEY_TOOT_STATUS_CONTENT) |
284 | 320 | ||
285 | return await _process_comment(context) | 321 | return await _process_comment(context) |
@@ -3,6 +3,7 @@ from telegram.constants import ParseMode | |||
3 | from telegram.error import BadRequest | 3 | from telegram.error import BadRequest |
4 | from telegram.ext import ContextTypes, ConversationHandler | 4 | from telegram.ext import ContextTypes, ConversationHandler |
5 | from dbstore.peewee_store import get_user_access_key, get_user_home_instance, delete_user_by_id, update_user_visibility | 5 | from dbstore.peewee_store import get_user_access_key, get_user_home_instance, delete_user_by_id, update_user_visibility |
6 | from dbstore.peewee_store import get_user_by_id, update_delayed_checkin | ||
6 | from dbstore.peewee_store import TOOT_VISIBILITY_PRIVATE, TOOT_VISIBILITY_UNLISTED, TOOT_VISIBILITY_PUBLIC | 7 | from dbstore.peewee_store import TOOT_VISIBILITY_PRIVATE, TOOT_VISIBILITY_UNLISTED, TOOT_VISIBILITY_PUBLIC |
7 | from config import * | 8 | from config import * |
8 | from util import check_user | 9 | from util import check_user |
@@ -54,7 +55,27 @@ async def logout_command(update: Update, context: ContextTypes.DEFAULT_TYPE, use | |||
54 | await update.message.reply_text(PROMPT_LOGOUT_SUCCESS, parse_mode=ParseMode.HTML, reply_markup=LOGIN_MENU) | 55 | await update.message.reply_text(PROMPT_LOGOUT_SUCCESS, parse_mode=ParseMode.HTML, reply_markup=LOGIN_MENU) |
55 | 56 | ||
56 | 57 | ||
57 | @check_user | 58 | async def delayed_checkin_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: |
59 | await update.message.reply_text(PROMPT_DELAYED_CHECKIN, parse_mode=ParseMode.HTML) | ||
60 | return DELAYED_CHECKIN | ||
61 | |||
62 | |||
63 | async def callback_delayed_checkin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int: | ||
64 | try: | ||
65 | delayed_minutes = int(update.effective_message.text) | ||
66 | except ValueError: | ||
67 | await update.message.edit_text(text="Integer expected, try again") | ||
68 | return DELAYED_CHECKIN | ||
69 | |||
70 | if delayed_minutes < 5: | ||
71 | delayed_minutes = 0 | ||
72 | |||
73 | update_delayed_checkin(str(update.effective_user.id), delayed_minutes) | ||
74 | u = get_user_by_id(str(update.effective_user.id)) | ||
75 | await update.message.reply_text(text=f"Delayed check-in set to {u['delayed_checkin']} minutes") | ||
76 | return ConversationHandler.END | ||
77 | |||
78 | |||
58 | async def toggle_visibility_command(update: Update, context: ContextTypes.DEFAULT_TYPE, user: User) -> int: | 79 | async def toggle_visibility_command(update: Update, context: ContextTypes.DEFAULT_TYPE, user: User) -> int: |
59 | visibility_menu = InlineKeyboardMarkup([ | 80 | visibility_menu = InlineKeyboardMarkup([ |
60 | [InlineKeyboardButton("Private", callback_data=TOOT_VISIBILITY_PRIVATE)], | 81 | [InlineKeyboardButton("Private", callback_data=TOOT_VISIBILITY_PRIVATE)], |
@@ -18,7 +18,11 @@ BOT_PORT = int(config["API"]["BOT_PORT"]) | |||
18 | 18 | ||
19 | MEDIA_GROUP_TIMEOUT = 3 | 19 | MEDIA_GROUP_TIMEOUT = 3 |
20 | 20 | ||
21 | FEDI_LOGIN, WAIT_VISIBILITY, WAIT_LOCATION, LOCATION_SEARCH_KEYWORD, LOCATION_CONFIRMATION, ADD_MEDIA, ADD_COMMENT = range(7) | 21 | FEDI_LOGIN, \ |
22 | WAIT_VISIBILITY, \ | ||
23 | DELAYED_CHECKIN, \ | ||
24 | WAIT_LOCATION, LOCATION_SEARCH_KEYWORD, LOCATION_CONFIRMATION, \ | ||
25 | ADD_MEDIA, ADD_COMMENT = range(8) | ||
22 | 26 | ||
23 | MAIN_MENU = ReplyKeyboardMarkup([ | 27 | MAIN_MENU = ReplyKeyboardMarkup([ |
24 | [KeyboardButton(text="Check-in here", request_location=True)], | 28 | [KeyboardButton(text="Check-in here", request_location=True)], |
@@ -46,5 +50,10 @@ class MsgDict(TypedDict): | |||
46 | 50 | ||
47 | KEY_TOOT_STATUS_ID = "toot_status_id" | 51 | KEY_TOOT_STATUS_ID = "toot_status_id" |
48 | KEY_TOOT_STATUS_CONTENT = "toot_status_content" | 52 | KEY_TOOT_STATUS_CONTENT = "toot_status_content" |
53 | KEY_IS_SCHEDULED_TOOT = "is_scheduled_toot" | ||
49 | 54 | ||
50 | BOT_SCOPE = ['read:accounts', 'write:media', 'write:statuses'] | 55 | # TODO: |
56 | # use the first scope as default | ||
57 | # if user set delayed post, ask user consent and request OAuth token again | ||
58 | # DEFAULT_BOT_SCOPE = ['read:accounts', 'write:media', 'write:statuses'] | ||
59 | 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): | |||
23 | client_id = CharField(max_length=128) | 23 | client_id = CharField(max_length=128) |
24 | client_secret = CharField(max_length=128) | 24 | client_secret = CharField(max_length=128) |
25 | toot_visibility = CharField(max_length=128, default=TOOT_VISIBILITY_PRIVATE) | 25 | toot_visibility = CharField(max_length=128, default=TOOT_VISIBILITY_PRIVATE) |
26 | # delayed checkin in minutes | ||
27 | delayed_checkin = IntegerField(default=0) | ||
28 | |||
29 | |||
30 | def update_delayed_checkin(telegram_user_id: str, delayed_checkin: int) -> int: | ||
31 | with db.connection_context(): | ||
32 | return User.update(delayed_checkin=delayed_checkin).where( | ||
33 | User.telegram_user_id == telegram_user_id | ||
34 | ).execute() | ||
26 | 35 | ||
27 | 36 | ||
28 | def update_user_visibility(telegram_user_id: str, visibility: str) -> int: | 37 | 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: | |||
45 | "client_id": user.client_id, | 54 | "client_id": user.client_id, |
46 | "client_secret": user.client_secret, | 55 | "client_secret": user.client_secret, |
47 | "toot_visibility": user.toot_visibility, | 56 | "toot_visibility": user.toot_visibility, |
57 | "delayed_checkin": user.delayed_checkin, | ||
48 | } | 58 | } |
49 | except DoesNotExist: | 59 | except DoesNotExist: |
50 | return {} | 60 | 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:" \ | |||
37 | "\n`/list` - list current linked Fediverse accounts" \ | 37 | "\n`/list` - list current linked Fediverse accounts" \ |
38 | "\n`/logout` - logout from specified Fediverse account, default logout from all" \ | 38 | "\n`/logout` - logout from specified Fediverse account, default logout from all" \ |
39 | "\n`/vis` - toggle visibility of your checkins on specified instances, default=private" \ | 39 | "\n`/vis` - toggle visibility of your checkins on specified instances, default=private" \ |
40 | "\n`/delay` - set delayed checkin in minutes, default=0" \ | ||
40 | "\n`/tos` - show ToS message" \ | 41 | "\n`/tos` - show ToS message" \ |
41 | "\n`/cancel` - cancel current operation during checkins" | 42 | "\n`/cancel` - cancel current operation during checkins" |
42 | 43 | ||
44 | PROMPT_DELAYED_CHECKIN = "How many minutes to delay your checkin? By default, checkin will be posted immediately.\n" \ | ||
45 | "If you wish to set delayed checkin, at least <b>5</b> minutes is required. \n" \ | ||
46 | "Input less than 5 will be set to 0 (immediate checkin)." | ||
43 | PROMPT_LIST = "You are linked with the following Fediverse accounts:" | 47 | PROMPT_LIST = "You are linked with the following Fediverse accounts:" |
44 | PROMPT_LIST_NO_RESULT = "You are not linked with any Fediverse account yet. \n\n Input <code>/login</code> to login." | 48 | PROMPT_LIST_NO_RESULT = "You are not linked with any Fediverse account yet. \n\n Input <code>/login</code> to login." |
45 | PROMPT_LOGOUT = "Choose Fediverse account to logout" | 49 | PROMPT_LOGOUT = "Choose Fediverse account to logout" |