|
|
|
import ast |
|
import random |
|
import re |
|
from io import BytesIO |
|
|
|
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Message, Update |
|
from telegram.constants import MessageLimit, ParseMode |
|
from telegram.error import BadRequest |
|
from telegram.ext import ( |
|
CallbackQueryHandler, |
|
CommandHandler, |
|
ContextTypes, |
|
MessageHandler, |
|
filters, |
|
) |
|
from telegram.helpers import escape_markdown, mention_markdown |
|
|
|
import Database.sql.notes_sql as sql |
|
from Mikobot import DRAGONS, LOGGER, MESSAGE_DUMP, SUPPORT_CHAT, dispatcher, function |
|
from Mikobot.plugins.disable import DisableAbleCommandHandler |
|
from Mikobot.plugins.helper_funcs.chat_status import check_admin, connection_status |
|
from Mikobot.plugins.helper_funcs.misc import build_keyboard, revert_buttons |
|
from Mikobot.plugins.helper_funcs.msg_types import get_note_type |
|
from Mikobot.plugins.helper_funcs.string_handling import ( |
|
escape_invalid_curly_brackets, |
|
markdown_to_html, |
|
) |
|
|
|
from .cust_filters import MessageHandlerChecker |
|
|
|
|
|
|
|
FILE_MATCHER = re.compile(r"^###file_id(!photo)?###:(.*?)(?:\s|$)") |
|
STICKER_MATCHER = re.compile(r"^###sticker(!photo)?###:") |
|
BUTTON_MATCHER = re.compile(r"^###button(!photo)?###:(.*?)(?:\s|$)") |
|
MYFILE_MATCHER = re.compile(r"^###file(!photo)?###:") |
|
MYPHOTO_MATCHER = re.compile(r"^###photo(!photo)?###:") |
|
MYAUDIO_MATCHER = re.compile(r"^###audio(!photo)?###:") |
|
MYVOICE_MATCHER = re.compile(r"^###voice(!photo)?###:") |
|
MYVIDEO_MATCHER = re.compile(r"^###video(!photo)?###:") |
|
MYVIDEONOTE_MATCHER = re.compile(r"^###video_note(!photo)?###:") |
|
|
|
ENUM_FUNC_MAP = { |
|
sql.Types.TEXT.value: dispatcher.bot.send_message, |
|
sql.Types.BUTTON_TEXT.value: dispatcher.bot.send_message, |
|
sql.Types.STICKER.value: dispatcher.bot.send_sticker, |
|
sql.Types.DOCUMENT.value: dispatcher.bot.send_document, |
|
sql.Types.PHOTO.value: dispatcher.bot.send_photo, |
|
sql.Types.AUDIO.value: dispatcher.bot.send_audio, |
|
sql.Types.VOICE.value: dispatcher.bot.send_voice, |
|
sql.Types.VIDEO.value: dispatcher.bot.send_video, |
|
} |
|
|
|
|
|
|
|
async def get( |
|
update: Update, |
|
context: ContextTypes.DEFAULT_TYPE, |
|
notename, |
|
show_none=True, |
|
no_format=False, |
|
): |
|
bot = context.bot |
|
chat_id = update.effective_message.chat.id |
|
chat = update.effective_chat |
|
note_chat_id = update.effective_chat.id |
|
note = sql.get_note(note_chat_id, notename) |
|
message = update.effective_message |
|
|
|
if note: |
|
if MessageHandlerChecker.check_user(update.effective_user.id): |
|
return |
|
|
|
if ( |
|
message.reply_to_message |
|
and not message.reply_to_message.forum_topic_created |
|
): |
|
reply_id = message.reply_to_message.message_id |
|
else: |
|
reply_id = message.message_id |
|
if note.is_reply: |
|
if MESSAGE_DUMP: |
|
try: |
|
await bot.forward_message( |
|
chat_id=chat_id, |
|
from_chat_id=MESSAGE_DUMP, |
|
message_id=note.value, |
|
) |
|
except BadRequest as excp: |
|
if excp.message == "Message to forward not found": |
|
await message.reply_text( |
|
"This message seems to have been lost - I'll remove it " |
|
"from your notes list.", |
|
) |
|
sql.rm_note(note_chat_id, notename) |
|
else: |
|
raise |
|
else: |
|
try: |
|
await bot.forward_message( |
|
chat_id=chat_id, |
|
from_chat_id=chat_id, |
|
message_id=markdown_to_html(note.value), |
|
) |
|
except BadRequest as excp: |
|
if excp.message == "Message to forward not found": |
|
await message.reply_text( |
|
"Looks like the original sender of this note has deleted " |
|
"their message - sorry! Get your bot admin to start using a " |
|
"message dump to avoid this. I'll remove this note from " |
|
"your saved notes.", |
|
) |
|
sql.rm_note(note_chat_id, notename) |
|
else: |
|
raise |
|
else: |
|
VALID_NOTE_FORMATTERS = [ |
|
"first", |
|
"last", |
|
"fullname", |
|
"username", |
|
"id", |
|
"chatname", |
|
"mention", |
|
] |
|
valid_format = escape_invalid_curly_brackets( |
|
note.value, |
|
VALID_NOTE_FORMATTERS, |
|
) |
|
if valid_format: |
|
if not no_format: |
|
if "%%%" in valid_format: |
|
split = valid_format.split("%%%") |
|
if all(split): |
|
text = random.choice(split) |
|
else: |
|
text = valid_format |
|
else: |
|
text = valid_format |
|
else: |
|
text = valid_format |
|
text = text.format( |
|
first=escape_markdown(message.from_user.first_name), |
|
last=escape_markdown( |
|
message.from_user.last_name or message.from_user.first_name, |
|
), |
|
fullname=escape_markdown( |
|
" ".join( |
|
( |
|
[ |
|
message.from_user.first_name, |
|
message.from_user.last_name, |
|
] |
|
if message.from_user.last_name |
|
else [message.from_user.first_name] |
|
), |
|
), |
|
), |
|
username=( |
|
"@" + message.from_user.username |
|
if message.from_user.username |
|
else mention_markdown( |
|
message.from_user.id, |
|
message.from_user.first_name, |
|
) |
|
), |
|
mention=mention_markdown( |
|
message.from_user.id, |
|
message.from_user.first_name, |
|
), |
|
chatname=escape_markdown( |
|
( |
|
message.chat.title |
|
if message.chat.type != "private" |
|
else message.from_user.first_name |
|
), |
|
), |
|
id=message.from_user.id, |
|
) |
|
else: |
|
text = "" |
|
|
|
keyb = [] |
|
parseMode = ParseMode.HTML |
|
buttons = sql.get_buttons(note_chat_id, notename) |
|
if no_format: |
|
parseMode = None |
|
text += revert_buttons(buttons) |
|
else: |
|
keyb = build_keyboard(buttons) |
|
|
|
keyboard = InlineKeyboardMarkup(keyb) |
|
|
|
try: |
|
if note.msgtype in (sql.Types.BUTTON_TEXT, sql.Types.TEXT): |
|
await bot.send_message( |
|
chat_id, |
|
markdown_to_html(text), |
|
reply_to_message_id=reply_id, |
|
parse_mode=parseMode, |
|
disable_web_page_preview=True, |
|
reply_markup=keyboard, |
|
message_thread_id=( |
|
message.message_thread_id if chat.is_forum else None |
|
), |
|
) |
|
else: |
|
await ENUM_FUNC_MAP[note.msgtype]( |
|
chat_id, |
|
note.file, |
|
caption=markdown_to_html(text), |
|
reply_to_message_id=reply_id, |
|
parse_mode=parseMode, |
|
disable_web_page_preview=True, |
|
reply_markup=keyboard, |
|
message_thread_id=( |
|
message.message_thread_id if chat.is_forum else None |
|
), |
|
) |
|
|
|
except BadRequest as excp: |
|
if excp.message == "Entity_mention_user_invalid": |
|
await message.reply_text( |
|
"Looks like you tried to mention someone I've never seen before. If you really " |
|
"want to mention them, forward one of their messages to me, and I'll be able " |
|
"to tag them!", |
|
) |
|
elif FILE_MATCHER.match(note.value): |
|
await message.reply_text( |
|
"This note was an incorrectly imported file from another bot - I can't use " |
|
"it. If you really need it, you'll have to save it again. In " |
|
"the meantime, I'll remove it from your notes list.", |
|
) |
|
sql.rm_note(note_chat_id, notename) |
|
else: |
|
await message.reply_text( |
|
"This note could not be sent, as it is incorrectly formatted. Ask in " |
|
f"@{SUPPORT_CHAT} if you can't figure out why!", |
|
) |
|
LOGGER.exception( |
|
"Could not parse message #%s in chat %s", |
|
notename, |
|
str(note_chat_id), |
|
) |
|
LOGGER.warning("Message was: %s", str(note.value)) |
|
return |
|
elif show_none: |
|
await message.reply_text("This note doesn't exist") |
|
|
|
|
|
@connection_status |
|
async def cmd_get(update: Update, context: ContextTypes.DEFAULT_TYPE): |
|
bot, args = context.bot, context.args |
|
if len(args) >= 2 and args[1].lower() == "noformat": |
|
await get(update, context, args[0].lower(), show_none=True, no_format=True) |
|
elif len(args) >= 1: |
|
await get(update, context, args[0].lower(), show_none=True) |
|
else: |
|
await update.effective_message.reply_text("Get rekt") |
|
|
|
|
|
@connection_status |
|
async def hash_get(update: Update, context: ContextTypes.DEFAULT_TYPE): |
|
message = update.effective_message.text |
|
fst_word = message.split()[0] |
|
no_hash = fst_word[1:].lower() |
|
await get(update, context, no_hash, show_none=False) |
|
|
|
|
|
@connection_status |
|
async def slash_get(update: Update, context: ContextTypes.DEFAULT_TYPE): |
|
message, chat_id = update.effective_message.text, update.effective_chat.id |
|
no_slash = message[1:] |
|
note_list = sql.get_all_chat_notes(chat_id) |
|
|
|
try: |
|
noteid = note_list[int(no_slash) - 1] |
|
note_name = str(noteid).strip(">").split()[1] |
|
await get(update, context, note_name, show_none=False) |
|
except IndexError: |
|
await update.effective_message.reply_text("Wrong Note ID 😾") |
|
|
|
|
|
@connection_status |
|
@check_admin(is_user=True) |
|
async def save(update: Update, context: ContextTypes.DEFAULT_TYPE): |
|
chat_id = update.effective_chat.id |
|
msg = update.effective_message |
|
if len(context.args) < 1: |
|
await msg.reply_text("You should give the note a name.") |
|
return |
|
|
|
note_name, text, data_type, content, buttons = get_note_type(msg) |
|
note_name = note_name.lower() |
|
if data_type is None: |
|
await msg.reply_text("Dude, there's no note content") |
|
return |
|
|
|
sql.add_note_to_db( |
|
chat_id, |
|
note_name, |
|
text, |
|
data_type, |
|
buttons=buttons, |
|
file=content, |
|
) |
|
|
|
await msg.reply_text( |
|
f"Yas! Added `{note_name}`.\nGet it with /get `{note_name}`, or `#{note_name}`", |
|
parse_mode=ParseMode.MARKDOWN, |
|
) |
|
|
|
if ( |
|
msg.reply_to_message |
|
and msg.reply_to_message.from_user.is_bot |
|
and not msg.reply_to_message.forum_topic_created |
|
): |
|
if text: |
|
await msg.reply_text( |
|
"Seems like you're trying to save a message from a bot. Unfortunately, " |
|
"bots can't forward bot messages, so I can't save the exact message. " |
|
"\nI'll save all the text I can, but if you want more, you'll have to " |
|
"forward the message yourself, and then save it.", |
|
) |
|
else: |
|
await msg.reply_text( |
|
"Bots are kinda handicapped by telegram, making it hard for bots to " |
|
"interact with other bots, so I can't save this message " |
|
"like I usually would - do you mind forwarding it and " |
|
"then saving that new message? Thanks!", |
|
) |
|
return |
|
|
|
|
|
@connection_status |
|
@check_admin(is_user=True) |
|
async def clear(update: Update, context: ContextTypes.DEFAULT_TYPE): |
|
args = context.args |
|
chat_id = update.effective_chat.id |
|
if len(args) >= 1: |
|
notename = args[0].lower() |
|
|
|
if sql.rm_note(chat_id, notename): |
|
await update.effective_message.reply_text("Successfully removed note.") |
|
else: |
|
await update.effective_message.reply_text( |
|
"That's not a note in my database!" |
|
) |
|
|
|
|
|
async def clearall(update: Update, context: ContextTypes.DEFAULT_TYPE): |
|
chat = update.effective_chat |
|
user = update.effective_user |
|
member = await chat.get_member(user.id) |
|
if member.status != "creator" and user.id not in DRAGONS: |
|
await update.effective_message.reply_text( |
|
"Only the chat owner can clear all notes at once.", |
|
) |
|
else: |
|
buttons = InlineKeyboardMarkup( |
|
[ |
|
[ |
|
InlineKeyboardButton( |
|
text="Delete all notes", |
|
callback_data="notes_rmall", |
|
), |
|
], |
|
[InlineKeyboardButton(text="Cancel", callback_data="notes_cancel")], |
|
], |
|
) |
|
await update.effective_message.reply_text( |
|
f"Are you sure you would like to clear ALL notes in {chat.title}? This action cannot be undone.", |
|
reply_markup=buttons, |
|
parse_mode=ParseMode.MARKDOWN, |
|
) |
|
|
|
|
|
async def clearall_btn(update: Update, context: ContextTypes.DEFAULT_TYPE): |
|
query = update.callback_query |
|
chat = update.effective_chat |
|
message = update.effective_message |
|
member = await chat.get_member(query.from_user.id) |
|
if query.data == "notes_rmall": |
|
if member.status == "creator" or query.from_user.id in DRAGONS: |
|
note_list = sql.get_all_chat_notes(chat.id) |
|
try: |
|
for notename in note_list: |
|
note = notename.name.lower() |
|
sql.rm_note(chat.id, note) |
|
await message.edit_text("Deleted all notes.") |
|
except BadRequest: |
|
return |
|
|
|
if member.status == "administrator": |
|
await query.answer("Only owner of the chat can do this.") |
|
|
|
if member.status == "member": |
|
await query.answer("You need to be admin to do this.") |
|
elif query.data == "notes_cancel": |
|
if member.status == "creator" or query.from_user.id in DRAGONS: |
|
await message.edit_text("Clearing of all notes has been cancelled.") |
|
return |
|
if member.status == "administrator": |
|
await query.answer("Only owner of the chat can do this.") |
|
if member.status == "member": |
|
await query.answer("You need to be admin to do this.") |
|
|
|
|
|
@connection_status |
|
async def list_notes(update: Update, context: ContextTypes.DEFAULT_TYPE): |
|
chat_id = update.effective_chat.id |
|
note_list = sql.get_all_chat_notes(chat_id) |
|
notes = len(note_list) + 1 |
|
msg = "Get note by `/notenumber` or `#notename` \n\n *ID* *Note* \n" |
|
for note_id, note in zip(range(1, notes), note_list): |
|
if note_id < 10: |
|
note_name = f"`{note_id:2}.` `#{(note.name.lower())}`\n" |
|
else: |
|
note_name = f"`{note_id}.` `#{(note.name.lower())}`\n" |
|
if len(msg) + len(note_name) > MessageLimit.MAX_TEXT_LENGTH: |
|
await update.effective_message.reply_text( |
|
msg, parse_mode=ParseMode.MARKDOWN |
|
) |
|
msg = "" |
|
msg += note_name |
|
|
|
if not note_list: |
|
try: |
|
await update.effective_message.reply_text("No notes in this chat!") |
|
except BadRequest: |
|
await update.effective_message.reply_text( |
|
"No notes in this chat!", quote=False |
|
) |
|
|
|
elif len(msg) != 0: |
|
await update.effective_message.reply_text(msg, parse_mode=ParseMode.MARKDOWN) |
|
|
|
|
|
async def __import_data__(chat_id, data, message: Message): |
|
failures = [] |
|
for notename, notedata in data.get("extra", {}).items(): |
|
match = FILE_MATCHER.match(notedata) |
|
matchsticker = STICKER_MATCHER.match(notedata) |
|
matchbtn = BUTTON_MATCHER.match(notedata) |
|
matchfile = MYFILE_MATCHER.match(notedata) |
|
matchphoto = MYPHOTO_MATCHER.match(notedata) |
|
matchaudio = MYAUDIO_MATCHER.match(notedata) |
|
matchvoice = MYVOICE_MATCHER.match(notedata) |
|
matchvideo = MYVIDEO_MATCHER.match(notedata) |
|
matchvn = MYVIDEONOTE_MATCHER.match(notedata) |
|
|
|
if match: |
|
failures.append(notename) |
|
notedata = notedata[match.end() :].strip() |
|
if notedata: |
|
sql.add_note_to_db(chat_id, notename[1:], notedata, sql.Types.TEXT) |
|
elif matchsticker: |
|
content = notedata[matchsticker.end() :].strip() |
|
if content: |
|
sql.add_note_to_db( |
|
chat_id, |
|
notename[1:], |
|
notedata, |
|
sql.Types.STICKER, |
|
file=content, |
|
) |
|
elif matchbtn: |
|
parse = notedata[matchbtn.end() :].strip() |
|
notedata = parse.split("<###button###>")[0] |
|
buttons = parse.split("<###button###>")[1] |
|
buttons = ast.literal_eval(buttons) |
|
if buttons: |
|
sql.add_note_to_db( |
|
chat_id, |
|
notename[1:], |
|
notedata, |
|
sql.Types.BUTTON_TEXT, |
|
buttons=buttons, |
|
) |
|
elif matchfile: |
|
file = notedata[matchfile.end() :].strip() |
|
file = file.split("<###TYPESPLIT###>") |
|
notedata = file[1] |
|
content = file[0] |
|
if content: |
|
sql.add_note_to_db( |
|
chat_id, |
|
notename[1:], |
|
notedata, |
|
sql.Types.DOCUMENT, |
|
file=content, |
|
) |
|
elif matchphoto: |
|
photo = notedata[matchphoto.end() :].strip() |
|
photo = photo.split("<###TYPESPLIT###>") |
|
notedata = photo[1] |
|
content = photo[0] |
|
if content: |
|
sql.add_note_to_db( |
|
chat_id, |
|
notename[1:], |
|
notedata, |
|
sql.Types.PHOTO, |
|
file=content, |
|
) |
|
elif matchaudio: |
|
audio = notedata[matchaudio.end() :].strip() |
|
audio = audio.split("<###TYPESPLIT###>") |
|
notedata = audio[1] |
|
content = audio[0] |
|
if content: |
|
sql.add_note_to_db( |
|
chat_id, |
|
notename[1:], |
|
notedata, |
|
sql.Types.AUDIO, |
|
file=content, |
|
) |
|
elif matchvoice: |
|
voice = notedata[matchvoice.end() :].strip() |
|
voice = voice.split("<###TYPESPLIT###>") |
|
notedata = voice[1] |
|
content = voice[0] |
|
if content: |
|
sql.add_note_to_db( |
|
chat_id, |
|
notename[1:], |
|
notedata, |
|
sql.Types.VOICE, |
|
file=content, |
|
) |
|
elif matchvideo: |
|
video = notedata[matchvideo.end() :].strip() |
|
video = video.split("<###TYPESPLIT###>") |
|
notedata = video[1] |
|
content = video[0] |
|
if content: |
|
sql.add_note_to_db( |
|
chat_id, |
|
notename[1:], |
|
notedata, |
|
sql.Types.VIDEO, |
|
file=content, |
|
) |
|
elif matchvn: |
|
video_note = notedata[matchvn.end() :].strip() |
|
video_note = video_note.split("<###TYPESPLIT###>") |
|
notedata = video_note[1] |
|
content = video_note[0] |
|
if content: |
|
sql.add_note_to_db( |
|
chat_id, |
|
notename[1:], |
|
notedata, |
|
sql.Types.VIDEO_NOTE, |
|
file=content, |
|
) |
|
else: |
|
sql.add_note_to_db(chat_id, notename[1:], notedata, sql.Types.TEXT) |
|
|
|
if failures: |
|
with BytesIO(str.encode("\n".join(failures))) as output: |
|
output.name = "failed_imports.txt" |
|
await dispatcher.bot.send_document( |
|
chat_id, |
|
document=output, |
|
filename="failed_imports.txt", |
|
caption="These files/photos failed to import due to originating " |
|
"from another bot. This is a telegram API restriction, and can't " |
|
"be avoided. Sorry for the inconvenience!", |
|
message_thread_id=( |
|
message.message_thread_id if message.chat.is_forum else None |
|
), |
|
) |
|
|
|
|
|
def __stats__(): |
|
return f"• {sql.num_notes()} notes, across {sql.num_chats()} chats." |
|
|
|
|
|
def __migrate__(old_chat_id, new_chat_id): |
|
sql.migrate_chat(old_chat_id, new_chat_id) |
|
|
|
|
|
def __chat_settings__(chat_id, user_id): |
|
notes = sql.get_all_chat_notes(chat_id) |
|
return f"There are `{len(notes)}` notes in this chat." |
|
|
|
|
|
|
|
|
|
|
|
__help__ = """ |
|
» /get <notename> : get the note with this notename |
|
» #<notename> : same as /get |
|
» /notes or /saved : list all saved notes in this chat |
|
» /number : Will pull the note of that number in the list |
|
➠ If you would like to retrieve the contents of a note without any formatting, use `/get <notename> noformat`. This can \ |
|
be useful when updating a current note |
|
|
|
*Admins only:* |
|
» /save <notename> <notedata> : saves notedata as a note with name notename |
|
➠ A button can be added to a note by using standard markdown link syntax - the link should just be prepended with a \ |
|
`buttonurl:` section, as such: `[somelink](buttonurl:example.com)`. Check `/markdownhelp` for more info |
|
» /save <notename> : save the replied message as a note with name notename |
|
Separate diff replies by `%%%` to get random notes |
|
➠ *Example:* |
|
`/save notename |
|
Reply 1 |
|
%%% |
|
Reply 2 |
|
%%% |
|
Reply 3` |
|
» /clear <notename>: clear note with this name |
|
» /removeallnotes: removes all notes from the group |
|
➠ *Note:* Note names are case-insensitive, and they are automatically converted to lowercase before getting saved. |
|
|
|
""" |
|
|
|
__mod_name__ = "NOTES" |
|
|
|
|
|
function(CommandHandler("get", cmd_get)) |
|
function(MessageHandler(filters.Regex(r"^#[^\s]+"), hash_get, block=False)) |
|
function(MessageHandler(filters.Regex(r"^/\d+$"), slash_get, block=False)) |
|
function(CommandHandler("save", save, block=False)) |
|
function(CommandHandler("clear", clear, block=False)) |
|
|
|
function( |
|
DisableAbleCommandHandler( |
|
["notes", "saved"], list_notes, admin_ok=True, block=False |
|
) |
|
) |
|
|
|
function(DisableAbleCommandHandler("removeallnotes", clearall, block=False)) |
|
function(CallbackQueryHandler(clearall_btn, pattern=r"notes_.*", block=False)) |
|
|
|
|