Initial commit

This commit is contained in:
blank X 2020-10-16 13:12:56 +07:00
commit f578c30ccc
27 changed files with 1823 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
__pycache__/
sessions/
config.yaml

16
example-config.yaml Normal file
View File

@ -0,0 +1,16 @@
telegram:
api_id: 0
api_hash: https://my.telegram.org
slave_bot_token: https://t.me/BotFather
config:
prefixes:
- .
sessions:
- blankie
- knees
- nezuko
log_chat: -1001278205033
spamwatch_api: https://t.me/SpamWatchBot
log_user_joins: false
log_user_adds: true
log_reports: true

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
pyrogram
tgcrypto
requests
aiohttp
PyYAML

183
sukuinote/__init__.py Normal file
View File

@ -0,0 +1,183 @@
import os
import html
import time
import logging
import asyncio
import traceback
import functools
import yaml
import aiohttp
from datetime import timedelta
from pyrogram import Client, StopPropagation, ContinuePropagation
from pyrogram.types import Chat, User
from pyrogram.parser import parser
from pyrogram.errors.exceptions.bad_request_400 import PeerIdInvalid, ChannelInvalid
logging.basicConfig(level=logging.INFO)
with open('config.yaml') as config:
config = yaml.safe_load(config)
loop = asyncio.get_event_loop()
help_dict = dict()
apps = []
app_user_ids = dict()
# this code here exists because i can't be fucked
class Parser(parser.Parser):
async def parse(self, text, mode):
if mode == 'through':
return text
return await super().parse(text, mode)
for session_name in config['config']['sessions']:
app = Client(session_name, api_id=config['telegram']['api_id'], api_hash=config['telegram']['api_hash'], plugins={'root': os.path.join(__package__, 'plugins')}, parse_mode='html', workdir='sessions')
app.parser = Parser(app)
apps.append(app)
slave = Client('sukuinote-slave', api_id=config['telegram']['api_id'], api_hash=config['telegram']['api_hash'], plugins={'root': os.path.join(__package__, 'slave-plugins')}, parse_mode='html', bot_token=config['telegram']['slave_bot_token'], workdir='sessions')
slave.parser = Parser(slave)
session = aiohttp.ClientSession()
async def get_entity(client, entity):
entity_client = client
if not isinstance(entity, Chat):
try:
entity = int(entity)
except ValueError:
pass
except TypeError:
entity = entity.id
try:
entity = await client.get_chat(entity)
except (PeerIdInvalid, ChannelInvalid):
for app in apps:
if app != client:
try:
entity = await app.get_chat(entity)
except (PeerIdInvalid, ChannelInvalid):
pass
else:
entity_client = app
break
else:
entity = await slave.get_chat(entity)
entity_client = slave
return entity, entity_client
async def get_user(client, entity):
entity_client = client
if not isinstance(entity, User):
try:
entity = int(entity)
except ValueError:
pass
except TypeError:
entity = entity.id
try:
entity = await client.get_users(entity)
except PeerIdInvalid:
for app in apps:
if app != client:
try:
entity = await app.get_users(entity)
except PeerIdInvalid:
pass
else:
entity_client = app
break
else:
entity = await slave.get_users(entity)
entity_client = slave
return entity, entity_client
def log_errors(func):
@functools.wraps(func)
async def wrapper(client, *args):
try:
await func(client, *args)
except (StopPropagation, ContinuePropagation):
raise
except Exception:
tb = traceback.format_exc()
try:
await slave.send_message(config['config']['log_chat'], f'Exception occured in {func.__name__}\n\n{tb}', parse_mode=None)
except Exception:
logging.exception('Failed to log exception for %s as slave', func.__name__)
tb = traceback.format_exc()
for app in apps:
try:
await app.send_message(config['config']['log_chat'], f'Exception occured in {func.__name__}\n\n{tb}', parse_mode=None)
except Exception:
logging.exception('Failed to log exception for %s as app', func.__name__)
tb = traceback.format_exc()
else:
break
raise
raise
return wrapper
def public_log_errors(func):
@functools.wraps(func)
async def wrapper(client, message):
try:
await func(client, message)
except (StopPropagation, ContinuePropagation):
raise
except Exception:
await message.reply_text(traceback.format_exc(), parse_mode=None)
raise
return wrapper
# https://stackoverflow.com/a/49361727
def format_bytes(size):
size = int(size)
# 2**10 = 1024
power = 1000
n = 0
power_labels = {0 : '', 1: 'K', 2: 'M', 3: 'G', 4: 'T'}
while size > power:
size /= power
n += 1
return f"{size:.2f} {power_labels[n]+'B'}"
# https://stackoverflow.com/a/34325723
def return_progress_string(current, total):
filled_length = int(30 * current // total)
return '[' + '=' * filled_length + ' ' * (30 - filled_length) + ']'
# https://stackoverflow.com/a/852718
# https://stackoverflow.com/a/775095
def calculate_eta(current, total, start_time):
if not current:
return '00:00:00'
end_time = time.time()
elapsed_time = end_time - start_time
seconds = (elapsed_time * (total / current)) - elapsed_time
thing = ''.join(str(timedelta(seconds=seconds)).split('.')[:-1]).split(', ')
thing[-1] = thing[-1].rjust(8, '0')
return ', '.join(thing)
progress_callback_data = dict()
async def progress_callback(current, total, reply, text, upload):
message_identifier = (reply.chat.id, reply.message_id)
last_edit_time, prevtext, start_time = progress_callback_data.get(message_identifier, (0, None, time.time()))
if current == total:
try:
progress_callback_data.pop(message_identifier)
except KeyError:
pass
elif (time.time() - last_edit_time) > 1:
handle = 'Upload' if upload else 'Download'
if last_edit_time:
speed = format_bytes((total - current) / (time.time() - start_time))
else:
speed = '0 B'
text = f'''{text}
<code>{return_progress_string(current, total)}</code>
<b>Total Size:</b> {format_bytes(total)}
<b>{handle}ed Size:</b> {format_bytes(current)}
<b>{handle} Speed:</b> {speed}/s
<b>ETA:</b> {calculate_eta(current, total, start_time)}'''
if prevtext != text:
await reply.edit_text(text)
prevtext = text
last_edit_time = time.time()
progress_callback_data[message_identifier] = last_edit_time, prevtext, start_time

22
sukuinote/__main__.py Normal file
View File

@ -0,0 +1,22 @@
import asyncio
from pyrogram import idle
from . import loop, apps, slave, app_user_ids, session
async def main():
async def _start_app(app):
await app.start()
asyncio.create_task(_get_me_loop(app))
async def _get_me_loop(app):
while True:
try:
me = await app.get_me()
app_user_ids[me.id] = me
except:
pass
await asyncio.sleep(60)
await asyncio.gather(*(_start_app(app) for app in apps), slave.start())
await idle()
await asyncio.gather(*(app.stop() for app in apps), slave.stop())
await session.close()
loop.run_until_complete(main())

View File

@ -0,0 +1,54 @@
import html
from pyrogram import Client, filters
from .. import config, help_dict, log_errors, public_log_errors, get_entity
ZWS = '\u200B'
def _generate_sexy(entity, ping):
text = entity.first_name
if entity.last_name:
text += f' {entity.last_name}'
sexy_text = '<code>[DELETED]</code>' if entity.is_deleted else html.escape(text or 'Empty???')
if not entity.is_deleted:
if ping:
sexy_text = f'<a href="tg://user?id={entity.id}">{sexy_text}</a>'
elif entity.username:
sexy_text = f'<a href="https://t.me/{entity.username}">{sexy_text}</a>'
elif not ping:
sexy_text = sexy_text.replace('@', f'@{ZWS}')
if entity.is_bot:
sexy_text += ' <code>[BOT]</code>'
if entity.is_verified:
sexy_text += ' <code>[VERIFIED]</code>'
if entity.is_support:
sexy_text += ' <code>[SUPPORT]</code>'
if entity.is_scam:
sexy_text += ' <code>[SCAM]</code>'
return sexy_text
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.outgoing & filters.command(['admin', 'admins'], prefixes=config['config']['prefixes']))
@log_errors
@public_log_errors
async def admins(client, message):
chat, entity_client = message.chat, client
command = message.command
command.pop(0)
if command:
chat = ' '.join(command)
try:
chat = int(chat)
except ValueError:
pass
chat, entity_client = await get_entity(client, chat)
text_unping = text_ping = ''
async for i in entity_client.iter_chat_members(chat.id, filter='administrators'):
text_unping += f'\n[<code>{i.user.id}</code>] {_generate_sexy(i.user, False)}'
text_ping += f'\n[<code>{i.user.id}</code>] {_generate_sexy(i.user, True)}'
if i.title:
text_unping += f' // {html.escape(i.title.replace("@", "@" + ZWS))}'
text_ping += f' // {html.escape(i.title)}'
reply = await message.reply_text(text_unping, disable_web_page_preview=True)
await reply.edit_text(text_ping, disable_web_page_preview=True)
help_dict['admins'] = ('Admins',
'''{prefix}admins <i>[chat]</i> - Lists the admins in <i>[chat]</i>
Aliases: {prefix}admin''')

View File

@ -0,0 +1,48 @@
import html
from pyrogram import Client, filters
from pyrogram.types.messages_and_media import Photo
from pyrogram.errors.exceptions.forbidden_403 import Forbidden
from .. import slave, config, help_dict, log_errors, public_log_errors
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.outgoing & filters.command(['anilist', 'al', 'alc', 'alchar', 'alcharacter', 'anilistc', 'anilistchar', 'anilistcharacter'], prefixes=config['config']['prefixes']))
@log_errors
@public_log_errors
async def anilist(client, message):
bot = await slave.get_me()
query = message.command
page = 1
character = 'c' in query.pop(0)
if query and query[0].isnumeric():
page = int(query.pop(0))
page -= 1
if page < 0:
page = 0
elif page > 9:
page = 9
query = ' '.join(query)
if not query:
return
results = await client.get_inline_bot_results(bot.username or bot.id, f'al{"c" if character else ""} ' + query)
if not results.results:
await message.reply_text('No results')
return
try:
await message.reply_inline_bot_result(results.query_id, results.results[page].id)
except IndexError:
await message.reply_text(f'There are only {len(results.results)} results')
except Forbidden:
text = {'message': results.results[page].send_message.message, 'entities': results.results[page].send_message.entities}
try:
photo = Photo._parse(client, results.results[page].photo)
await message.reply_cached_media(photo.file_id, photo.file_ref, caption=text, parse_mode='through')
except Forbidden:
await message.reply_text(text, disable_web_page_preview=True, parse_mode='through')
help_dict['anilist'] = ('Anilist',
'''{prefix}anilist <i>&lt;query&gt;</i> - Searches for anime/manga named <i>&lt;query&gt;</i> on Anilist
Aliases: {prefix}al
Can also be activated inline with: @{bot} anilist <i>&lt;query&gt;</i> or @{bot} al <i>&lt;query&gt;</i>
{prefix}anilistc <i>&lt;query&gt;</i> - Searches for characters named <i>&lt;query&gt;</i> on Anilist
Aliases: {prefix}alc, alchar, alcharacter, anilistchar, anilistcharacter
Can also be activated inline with: @{bot} anilistc <i>&lt;query&gt;</i> or @{bot} alc <i>&lt;query&gt;</i>''')

32
sukuinote/plugins/cat.py Normal file
View File

@ -0,0 +1,32 @@
import html
import tempfile
from pyrogram import Client, filters
from .. import config, help_dict, log_errors, session, progress_callback, public_log_errors
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.outgoing & filters.command('cat', prefixes=config['config']['prefixes']))
@log_errors
@public_log_errors
async def cat(client, message):
media = message.document
if not media and not getattr(message.reply_to_message, 'empty', True):
media = message.reply_to_message.document
if not media:
await message.reply_text('Document required')
return
done = False
with tempfile.NamedTemporaryFile() as file:
reply = await message.reply_text('Downloading...')
await client.download_media(media, file_name=file.name, progress=progress_callback, progress_args=(reply, 'Downloading...', False))
with open(file.name) as nfile:
while True:
chunk = nfile.read(4096)
if not chunk:
break
chunk = f'<code>{html.escape(chunk)}</code>'
if done:
await message.reply_text(chunk, quote=False)
else:
await reply.edit_text(chunk)
done = True
help_dict['cat'] = ('cat', '{prefix}cat <i>(as caption of text file or reply)</i> - Outputs file\'s text to Telegram')

View File

@ -0,0 +1,65 @@
import html
from pyrogram import Client, filters
from .. import config, help_dict, log_errors, public_log_errors
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.outgoing & filters.command(['d', 'del', 'delete'], prefixes=config['config']['prefixes']))
@log_errors
@public_log_errors
async def delete(client, message):
messages = set((message.message_id,))
reply = message.reply_to_message
if not getattr(reply, 'empty', True):
messages.add(reply.message_id)
await client.delete_messages(message.chat.id, messages)
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.outgoing & filters.command(['p', 'purge', 'sp', 'selfpurge'], prefixes=config['config']['prefixes']))
@log_errors
@public_log_errors
async def purge(client, message):
selfpurge = 's' in message.command[0]
ids = set((message.message_id,))
reply = message.reply_to_message
if not getattr(reply, 'empty', True):
for i in await client.get_messages(message.chat.id, range(reply.message_id, message.message_id), replies=0):
if selfpurge and not i.outgoing:
continue
ids.add(i.message_id)
await client.delete_messages(message.chat.id, ids)
yeetpurge_info = {True: dict(), False: dict()}
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.outgoing & filters.command(['yp', 'yeetpurge', 'syp', 'selfyeetpurge'], prefixes=config['config']['prefixes']))
@log_errors
@public_log_errors
async def yeetpurge(client, message):
reply = message.reply_to_message
if getattr(message, 'empty', True):
await message.delete()
return
info = yeetpurge_info['s' in message.command[0]]
if message.chat.id not in info:
resp = await message.reply_text('Reply to end destination')
info[message.chat.id] = (message.message_id, resp.message_id, reply.message_id)
return
og_message, og_resp, og_reply = info.pop(message.chat.id)
messages = set((og_message, og_resp, message.message_id))
for i in await client.get_messages(message.chat.id, range(og_reply, reply.message_id + 1), replies=0):
if 's' in message.command[0] and not i.outgoing:
continue
messages.add(i.message_id)
await client.delete_messages(message.chat.id, messages)
help_dict['delete'] = ('Delete',
'''{prefix}delete <i>(as reply to a message)</i> - Deletes the replied to message
Aliases: {prefix}d, {prefix}del
{prefix}purge <i>(as reply to a message)</i> - Purges the messages between the one you replied (and including the one you replied)
Aliases: {prefix}p
{prefix}selfpurge <i>(as reply to a message)</i> - {prefix}p but only your messages
Aliases: {prefix}sp
{prefix}yeetpurge <i>(as reply to a message)</i> - Purges messages in between
Aliases: {prefix}yp
{prefix}selfyeetpurge <i>(as reply to a message)</i> - {prefix}yp but only your messages
Aliases: {prefix}syp''')

209
sukuinote/plugins/einfo.py Normal file
View File

@ -0,0 +1,209 @@
import re
import html
import asyncio
import datetime
from pyrogram import Client, filters
from .. import config, help_dict, get_entity, session, log_errors, public_log_errors
conversation_hack = dict()
DEAI_BAN_CODES = {
"00": "Gban",
"01": "Joinspam",
"02": "Spambot",
"03": "Generic spam",
"04": "Scam",
"05": "Illegal",
"06": "Pornography",
"07": "Nonsense",
"08": "Chain bans",
"09": "Special",
"10": "Preemptive",
"11": "Copyright",
"12": "Admin rights abuse",
"13": "Toxicity",
"14": "Flood",
"15": "Detected but not classified",
"16": "Advanced detection",
"17": "Reported",
"18": "AI association",
"19": "Impersonation",
"20": "Malware",
"21": "Ban evasion",
"22": "PM spam",
"23": "Spam adding members"
}
DEAI_MODULE_CODES = {
"0": "Gban",
"1": "Database parser",
"2": "Realtime",
"3": "Profiler",
"4": "Scraper",
"5": "Association analytics",
"6": "Codename Autobahn",
"7": "Codename Polizei",
"8": "Codename Gestapo"
}
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.outgoing & filters.command(['einfo', 'externalinfo', 'sw', 'spamwatch', 'deai', 'sp', 'spamprotection', 'cas', 'combot', 'rose'], prefixes=config['config']['prefixes']))
@log_errors
@public_log_errors
async def fedstat(client, message):
entity = message.from_user
args = message.command
command = args.pop(0).lower()
if args:
entity = ' '.join(args)
elif not getattr(message.reply_to_message, 'empty', True):
entity = message.reply_to_message.from_user or entity
if isinstance(entity, str) and (not entity.isnumeric() and not entity.startswith('TEL-')):
entity, entity_client = await get_entity(client, entity)
if not isinstance(entity, str):
entity = str(entity.id)
if entity.startswith('TEL-') or int(entity) < 0 or command in ('sp', 'spamprotection'):
await message.reply_text(f'Spam Protection:\n{await get_spam_protection(entity)}')
elif command in ('sw', 'spamwatch'):
await message.reply_text(f'Spamwatch:\n{await get_spamwatch(entity)}')
elif command == 'deai':
await message.reply_text(f'DEAI:\n{await get_deai(client, entity)}')
elif command == 'rose':
await message.reply_text(f'Rose Support:\n{await get_rose(client, entity)}')
elif command in ('cas', 'combot'):
await message.reply_text(f'CAS:\n{await get_cas(entity)}')
else:
spamwatch, deai, cas, spam_protection, rose = await asyncio.gather(get_spamwatch(entity), get_deai(client, entity), get_cas(entity), get_spam_protection(entity), get_rose(client, entity))
await message.reply_text(f'''Spamwatch:
{spamwatch}
CAS:
{cas}
Rose Support:
{rose}
DEAI:
{deai}
Spam Protection:
{spam_protection}''')
async def get_spamwatch(entity):
async with session.get(f'https://api.spamwat.ch/banlist/{entity}', headers={'Authorization': f'Bearer {config["config"]["spamwatch_api"]}'}) as resp:
json = await resp.json()
if 'code' in json:
return f'- <b>{json["code"]}:</b> {html.escape(json.get("error", ""))}'
return f'''- <b>Banned on:</b> {str(datetime.datetime.fromtimestamp(json["date"]))}
- <b>Reason:</b> {html.escape(json["reason"].strip())}'''
async def get_rose(client, entity):
new_message = await client.send_message('missrose_bot', f'/fbanstat {entity} 86718661-6bfc-4bd0-9447-7c419eb08e69')
identifier = (new_message.chat.id, new_message.message_id)
conversation_hack[identifier] = None
while not conversation_hack[identifier]:
await asyncio.sleep(0.5)
ntext = conversation_hack[identifier].split('\n')
ntext.pop(0)
if ntext:
date = '-'.join(ntext.pop().split(' ')[-1].split('/')[::-1])
reason = '\n'.join(ntext).strip()
text = f'- <b>Banned on:</b> {date}'
if reason:
text += f'\n- <b>Reason:</b> {html.escape(reason)}'
return text
return '- <b>404:</b> Not Found'
async def get_deai(client, entity):
new_message = await client.send_message('rsophiebot', f'/fcheck {entity} 845d33d3-0961-4e44-b4b5-4c57775fbdac')
identifier = (new_message.chat.id, new_message.message_id)
conversation_hack[identifier] = None
while not conversation_hack[identifier]:
await asyncio.sleep(0.5)
ntext = conversation_hack[identifier].split('\n')
ntext.pop(0)
if ntext:
ntext.pop(0)
if ntext:
text = '- <b>Reason:</b> '
ntext.pop(0)
reason = '\n'.join(ntext).strip()
text += html.escape(reason) or 'None'
match = re.match(r'(?:AIdetection:)?((?:0x\d{2} )+)risk:(\S+) mod:X([0-8])(?: cmt:(.+))?', reason)
if match:
text += '\n- <b>Ban Codes:</b>\n'
for i in match.group(1).split(' '):
if i:
i = DEAI_BAN_CODES.get(i.strip()[2:], i.strip())
text += f'--- {i}\n'
text += f'- <b>Risk Factor:</b> {match.group(2).capitalize()}\n'
text += f'- <b>Module:</b> {DEAI_MODULE_CODES.get(match.group(3), match.group(3))}'
if (match.group(4) or '').strip():
text += f'\n- <b>Comment:</b> {html.escape(match.group(4).strip())}'
return text
return '- <b>404:</b> Not Found'
async def get_cas(entity):
async with session.get(f'https://api.cas.chat/check?user_id={entity}') as resp:
json = await resp.json()
if json['ok']:
return f'''- <b>Banned on:</b> {str(datetime.datetime.fromisoformat(json["result"]["time_added"][:-1]))}
- <b>Offenses:</b> {json["result"]["offenses"]}'''
return f'- <b>XXX:</b> {html.escape(json.get("description", "XXX"))}'
async def get_spam_protection(entity):
async with session.get(f'https://api.intellivoid.net/spamprotection/v1/lookup?query={entity}') as resp:
json = await resp.json()
if json['success']:
text = ''
if json['results']['attributes']['intellivoid_accounts_verified']:
text += '- <b>Intellivoid Account Linked:</b> Yes\n'
if json['results']['attributes']['is_potential_spammer']:
text += '- <b>Potential Spammer:</b> Yes\n'
if json['results']['attributes']['is_operator']:
text += '- <b>Operator:</b> Yes\n'
if json['results']['attributes']['is_agent']:
text += '- <b>Agent:</b> Yes\n'
if json['results']['attributes']['is_whitelisted']:
text += '- <b>Whitelisted:</b> Yes\n'
text += f'- <b>Ham/Spam Prediction:</b> {json["results"]["spam_prediction"]["ham_prediction"] or 0}/{json["results"]["spam_prediction"]["spam_prediction"] or 0}'
if json['results']['language_prediction']['language']:
text += f'''\n- <b>Language Prediction:</b> {json["results"]["language_prediction"]["language"]}
- <b>Language Prediction Probability:</b> {json["results"]["language_prediction"]["probability"]}'''
if json['results']['attributes']['is_blacklisted']:
text += f'''\n- <b>Blacklist Flag:</b> {json["results"]["attributes"]["blacklist_flag"]}
- <b>Blacklist Reason:</b> {json["results"]["attributes"]["blacklist_reason"]}'''
if json['results']['attributes']['original_private_id']:
text += f'\n- <b>Original Private ID:</b> {json["results"]["attributes"]["original_private_id"]}'
return text
return f'- <b>{json["response_code"]}</b>: {json["error"]["error_code"]}: {json["error"]["type"]}: {html.escape(json["error"]["message"])}'
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.chat(['rsophiebot', 'missrose_bot']) & filters.incoming & filters.regex('^Federation ban info:\n|You ain\'t fbanned in this fed\.|^Failed to get user: unable to getChatMember: Bad Request: chat not found$|^.+ is not banned in this fed\.$|^.+ is currently banned in Rose Support Official, for the following reason:\n\n'))
async def fedstat_conversation_hack(client, message):
reply = message.reply_to_message
if not getattr(reply, 'empty', True):
identifier = (reply.chat.id, reply.message_id)
if identifier in conversation_hack:
conversation_hack[identifier] = message.text
await client.read_history(message.chat.id, message.message_id)
help_dict['einfo'] = ('External Info',
'''{prefix}externalinfo <i>&lt;user&gt;</i> - Get extended info of <i>&lt;user&gt;</i>
{prefix}externalinfo <i>(as reply to message)</i> - Get extended info of replied user
Aliases: {prefix}extinfo, {prefix}einfo
{prefix}spamwatch <i>&lt;user&gt;</i> - Get Spamwatch info of <i>&lt;user&gt;</i>
{prefix}spamwatch <i>(as reply to message)</i> - Get Spamwatch info of replied user
Aliases: {prefix}sw
{prefix}cas <i>&lt;user&gt;</i> - Get Combot Anti Spam info of <i>&lt;user&gt;</i>
{prefix}cas <i>(as reply to message)</i> - Get Combot Anti Spam info of replied user
Aliases: {prefix}combot
{prefix}rose <i>&lt;user&gt;</i> - Get Rose Support Federation info of <i>&lt;user&gt;</i>
{prefix}rose <i>(as reply to message)</i> - Get Rose Support Federation info of replied user
{prefix}deai <i>&lt;user&gt;</i> - Get DEAI info of <i>&lt;user&gt;</i>
{prefix}deai <i>(as reply to message)</i> - Get DEAI info of replied user
{prefix}spamprotection <i>&lt;user&gt;</i> - Get Spam Protection info of <i>&lt;user&gt;</i>
{prefix}spamprotection <i>(as reply to message)</i> - Get Spam Protection info of replied user
Aliases: {prefix}sp''')

View File

@ -0,0 +1,88 @@
import os
import html
from pyrogram import Client, filters
from pyrogram.errors.exceptions.bad_request_400 import MessageIdInvalid
from .. import config, help_dict, log_errors, session, progress_callback, public_log_errors
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.outgoing & filters.command(['ls', 'hls', 'hiddenls'], prefixes=config['config']['prefixes']))
@log_errors
@public_log_errors
async def ls(client, message):
dir = os.path.abspath(os.path.expanduser(' '.join(message.command[1:]) or '.'))
text = ''
folders = []
files = []
try:
for i in sorted(os.listdir(dir)):
if i.startswith('.') and 'h' not in message.command[0]:
continue
(folders if os.path.isdir(os.path.join(dir, i)) else files).append(i)
except NotADirectoryError:
text = f'<code>{html.escape(os.path.basename(dir))}</code>'
except Exception as ex:
text = f'{type(ex).__name__}: {html.escape(str(ex))}'
else:
for i in folders:
text += f'<code>{html.escape(i)}</code>\n'
for i in files:
text += f'<code>{html.escape(i)}</code>\n'
await message.reply_text(text or 'Empty', disable_web_page_preview=True)
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.outgoing & filters.command(['ul', 'upload'], prefixes=config['config']['prefixes']))
@log_errors
@public_log_errors
async def upload(client, message):
file = os.path.expanduser(' '.join(message.command[1:]))
if not file:
return
text = f'Uploading {html.escape(file)}...'
reply = await message.reply_text(text)
try:
await client.send_document(message.chat.id, file, progress=progress_callback, progress_args=(reply, text, True), force_document=True, reply_to_message_id=None if message.chat.type in ('private', 'bot') else message.message_id)
except MessageIdInvalid:
await message.reply_text('Upload cancelled!')
else:
await reply.delete()
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.outgoing & filters.command(['dl', 'download'], prefixes=config['config']['prefixes']))
@log_errors
@public_log_errors
async def download(client, message):
file = os.path.expanduser(' '.join(message.command[1:]) or './')
if os.path.isdir(file):
file = os.path.join(file, '')
available_media = ("audio", "document", "photo", "sticker", "animation", "video", "voice", "video_note")
download_message = None
for i in available_media:
if getattr(message, i, None):
download_message = message
break
else:
reply = message.reply_to_message
if not getattr(reply, 'empty', True):
for i in available_media:
if getattr(reply, i, None):
download_message = reply
break
if download_message is None:
await message.reply_text('Media required')
return
text = 'Downloading...'
reply = await message.reply_text(text)
try:
file = await download_message.download(file, progress=progress_callback, progress_args=(reply, text, False))
except MessageIdInvalid:
await message.reply_text('Download cancelled!')
else:
await reply.edit_text(f'Downloaded to {html.escape(file)}')
help_dict['files'] = ('Files',
'''{prefix}ls <i>[directory]</i> - Lists files in <i>[directory]</i>
{prefix}hiddenls <i>[directory]</i> - {prefix}ls but shows hidden files
Aliases: {prefix}hls
{prefix}upload <i>&lt;file name&gt;</i> - Uploads <i>&lt;file name&gt;</i>
Aliases: {prefix}ul
{prefix}download <i>[file name]</i> <i>(as reply or caption to a file)</i> - Downloads file to <i>[file name]</i>
Aliases: {prefix}dl''')

40
sukuinote/plugins/help.py Normal file
View File

@ -0,0 +1,40 @@
import html
from pyrogram import Client, filters
from pyrogram.errors.exceptions.forbidden_403 import Forbidden
from .. import slave, config, help_dict, log_errors, public_log_errors
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.outgoing & filters.command('help', prefixes=config['config']['prefixes']))
@log_errors
@public_log_errors
async def help(client, message):
bot = await slave.get_me()
module = message.command
module.pop(0)
module = ' '.join(module).lower().strip()
results = await client.get_inline_bot_results(bot.username or bot.id, 'help')
for a, i in enumerate(results.results):
if a:
internal_name = i.id[5:].split('-')
internal_name.pop()
internal_name = '-'.join(internal_name).lower().strip()
external_name = i.title.lower().strip()
if module in (internal_name, external_name):
result = i
break
else:
result = results.results[0]
try:
await message.reply_inline_bot_result(results.query_id, result.id)
except Forbidden:
if module:
await message.reply_text({'message': result.send_message.message, 'entities': result.send_message.entities}, parse_mode='through')
else:
text = 'Avaliable plugins:\n'
for i in help_dict:
text += f'- {html.escape(help_dict[i][0])}\n'
await message.reply_text(text)
help_dict['help'] = ('Help',
'''{prefix}help - Shows list of plugins
{prefix}help <i>&lt;plugin name&gt;</i> - Shows help for <i>&lt;plugin name&gt;</i>
Can also be activated inline with: @{bot} help''')

113
sukuinote/plugins/info.py Normal file
View File

@ -0,0 +1,113 @@
import html
from pyrogram import Client, filters
from .. import config, help_dict, get_entity, log_errors, public_log_errors
ZWS = '\u200B'
def _generate_sexy(entity, ping):
text = getattr(entity, 'title', None)
if not text:
text = entity.first_name
if entity.last_name:
text += f' {entity.last_name}'
sexy_text = html.escape(text or 'Empty???')
if ping and entity.type in ('private', 'bot'):
sexy_text = f'<a href="tg://user?id={entity.id}">{sexy_text}</a>'
elif entity.username:
sexy_text = f'<a href="https://t.me/{entity.username}">{sexy_text}</a>'
elif not ping:
sexy_text = sexy_text.replace('@', f'@{ZWS}')
if entity.type == 'bot':
sexy_text += ' <code>[BOT]</code>'
if entity.is_verified:
sexy_text += ' <code>[VERIFIED]</code>'
if entity.is_support:
sexy_text += ' <code>[SUPPORT]</code>'
if entity.is_scam:
sexy_text += ' <code>[SCAM]</code>'
return sexy_text
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.outgoing & filters.command(['info', 'whois'], prefixes=config['config']['prefixes']))
@log_errors
@public_log_errors
async def info(client, message):
entity = message.chat
command = message.command
command.pop(0)
if command:
entity = ' '.join(command)
elif not getattr(message.reply_to_message, 'empty', True):
entity = message.reply_to_message.from_user or message.reply_to_message.chat
entity, entity_client = await get_entity(client, entity)
text_ping = _generate_sexy(entity, True)
text_unping = _generate_sexy(entity, False)
text_ping += f'\n<b>ID:</b> <code>{entity.id}</code>'
text_unping += f'\n<b>ID:</b> <code>{entity.id}</code>'
if entity.dc_id:
text_ping += f'\n<b>DC ID:</b> {entity.dc_id}'
text_unping += f'\n<b>DC ID:</b> {entity.dc_id}'
if entity.username:
text_ping += f'\n<b>Username:</b> @{entity.username}'
text_unping += f'\n<b>Username:</b> @{ZWS}{entity.username}'
if entity.members_count:
text_ping += f'\n<b>Members:</b> {entity.members_count}'
text_unping += f'\n<b>Members:</b> {entity.members_count}'
if entity.linked_chat:
text_ping += f'\n<b>Linked Chat:</b> {_generate_sexy(entity.linked_chat, False)} [<code>{entity.linked_chat.id}</code>]'
text_unping += f'\n<b>Linked Chat:</b> {_generate_sexy(entity.linked_chat, False)} [<code>{entity.linked_chat.id}</code>]'
if entity.description:
text_ping += f'\n<b>Description:</b>\n{html.escape(entity.description)}'
text_unping += f'\n<b>Description:</b>\n{html.escape(entity.description.replace("@", "@" + ZWS))}'
reply = await message.reply_text(text_unping, disable_web_page_preview=True)
if text_ping != text_unping:
await reply.edit_text(text_ping, disable_web_page_preview=True)
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.outgoing & filters.command('id', prefixes=config['config']['prefixes']))
@log_errors
@public_log_errors
async def id(client, message):
text_unping = '<b>Chat ID:</b>'
if message.chat.username:
text_unping = f'<a href="https://t.me/{message.chat.username}">{text_unping}</a>'
text_unping += f' <code>{message.chat.id}</code>\n'
text = '<b>Message ID:</b>'
if message.link:
text = f'<a href="{message.link}">{text}</a>'
text += f' <code>{message.message_id}</code>\n'
text_unping += text
if message.from_user:
text_unping += f'<b><a href="tg://user?id={message.from_user.id}">User ID:</a></b> <code>{message.from_user.id}</code>\n'
text_ping = text_unping
reply = message.reply_to_message
if not getattr(reply, 'empty', True):
text_unping += '\n'
text = '<b>Replied Message ID:</b>'
if reply.link:
text = f'<a href="{reply.link}">{text}</a>'
text += f' <code>{reply.message_id}</code>\n'
text_unping += text
text_ping = text_unping
if reply.from_user:
text = '<b>Replied User ID:</b>'
if reply.from_user.username:
text = f'<a href="https://t.me/{reply.from_user.username}">{text}</a>'
text += f' <code>{reply.from_user.id}</code>\n'
text_unping += text
text_ping += f'<b><a href="tg://user?id={reply.from_user.id}">Replied User ID:</a></b> <code>{reply.from_user.id}</code>\n'
if reply.forward_from:
text_unping += '\n'
text = '<b>Forwarded User ID:</b>'
if reply.forward_from.username:
text = f'<a href="https://t.me/{reply.forward_from.username}">{text}</a>'
text += f' <code>{reply.forward_from.id}</code>\n'
text_unping += text
text_ping += f'\n<b><a href="tg://user?id={reply.forward_from.id}">Forwarded User ID:</a></b> <code>{reply.forward_from.id}</code>\n'
reply = await message.reply_text(text_unping, disable_web_page_preview=True)
if text_unping != text_ping:
await reply.edit_text(text_ping, disable_web_page_preview=True)
help_dict['info'] = ('Info',
'''{prefix}info <i>&lt;entity&gt;</i> - Get entity info
{prefix}info <i>(as reply to message)</i> - Get entity info of replied user
Aliases: {prefix}whois
{prefix}id <i>[maybe reply to message]</i> - Gets IDs''')

View File

@ -0,0 +1,68 @@
import html
import asyncio
from pyrogram import Client, filters
from .. import config, help_dict, slave, log_errors
reported = set()
lock = asyncio.Lock()
@Client.on_message(filters.regex(r'(?:^|\s+)@admins?(?:$|\W+)|^[/!](?:report|admins?)(?:$|\W+)') & filters.group)
@log_errors
async def log_reports(client, message):
if not config['config']['log_reports']:
return
identifier = (message.chat.id, message.message_id)
async with lock:
if identifier in reported:
return
chat_text = html.escape(message.chat.title)
if message.chat.username:
chat_text = f'<a href="https://t.me/{message.chat.username}">{chat_text}</a>'
text = f'<b>Report Event</b>\n- <b>Chat:</b> {chat_text} '
if message.chat.is_verified:
chat_text += '<code>[VERIFIED]</code> '
if message.chat.is_support:
chat_text += '<code>[SUPPORT]</code> '
if message.chat.is_scam:
chat_text += '<code>[SCAM]</code> '
text += f'[<code>{message.chat.id}</code>]\n- <b>Reporter:</b> '
user_text = message.from_user.first_name
if message.from_user.last_name:
user_text += f' {message.from_user.last_name}'
user_text = '<code>[DELETED]</code>' if message.from_user.is_deleted else html.escape(user_text or 'Empty???')
if message.from_user.is_verified:
user_text += ' <code>[VERIFIED]</code>'
if message.from_user.is_support:
user_text += ' <code>[SUPPORT]</code>'
if message.from_user.is_scam:
user_text += ' <code>[SCAM]</code>'
text += f'{user_text} [<code>{message.from_user.id}</code>]\n'
start, end = message.matches[0].span()
text += f'- <b><a href="{message.link}">Report Message'
mtext = (message.text or message.caption or '').strip()
if start or end < len(mtext):
text += ':'
text += '</a></b>'
if start or end < len(mtext):
text += f' {html.escape(mtext.strip()[:1000])}'
reply = message.reply_to_message
if not getattr(reply, 'empty', True):
text += '\n- <b>Reportee:</b> '
user_text = message.reply_to_message.from_user.first_name
if message.reply_to_message.from_user.last_name:
user_text += f' {message.reply_to_message.from_user.last_name}'
user_text = '<code>[DELETED]</code>' if message.from_user.is_deleted else html.escape(user_text or 'Empty???')
if message.reply_to_message.from_user.is_verified:
user_text += ' <code>[VERIFIED]</code>'
if message.reply_to_message.from_user.is_support:
user_text += ' <code>[SUPPORT]</code>'
if message.reply_to_message.from_user.is_scam:
user_text += ' <code>[SCAM]</code>'
text += f'{user_text} [<code>{message.reply_to_message.from_user.id}</code>]\n- <b><a href="{message.reply_to_message.link}">Reported Message'
mtext = message.reply_to_message.text or message.reply_to_message.caption or ''
if mtext.strip():
text += ':'
text += f'</a></b> {html.escape(mtext.strip()[:1000])}'
reply = await slave.send_message(config['config']['log_chat'], text, disable_web_page_preview=True)
reported.add(identifier)
reported.add((reply.chat.id, reply.message_id))

View File

@ -0,0 +1,44 @@
import os
import requests
from pyrogram import Client, filters
from pyrogram.types.messages_and_media import Photo, Animation
from pyrogram.errors.exceptions.forbidden_403 import Forbidden
from .. import config, help_dict, log_errors, session, slave, public_log_errors
help_text = ''
resp = requests.get('https://nekos.life/api/v2/endpoints')
json = resp.json()
for i in json:
_, i = i.split(' ', 1)
i = i.strip()
if i.startswith('/api/v2/img/<\''):
for i in os.path.basename(i)[1:-1].split(', '):
i = i[1:-1]
if 'v3' in i:
continue
def _generate(i):
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.outgoing & filters.command(i, prefixes=config['config']['prefixes']))
@log_errors
@public_log_errors
async def func(client, message):
bot = await slave.get_me()
results = await client.get_inline_bot_results(bot.username or bot.id, i)
result = results.results[0]
if result.type == 'photo':
file = Photo._parse(client, result.photo)
else:
file = Animation._parse(client, result.document, result.document.attributes, 'hello.mp4')
try:
await message.reply_cached_media(file.file_id, file.file_ref, caption=result.send_message.message, parse_mode=None)
except Forbidden:
await message.reply_text(result.send_message.message, parse_mode=None)
return func
func = _generate(i)
globals()[i] = func
locals()[i] = func
func = None
help_text += '{prefix}' + i.lower() + f' - Gets a {"gif" if "gif" in i else "picture"} of {i.lower()}\n'
break
help_dict['nekos'] = ('Nekos.life', help_text + '\nCan also be activated inline with: @{bot} <i>&lt;command without dot&gt;</i>')

21
sukuinote/plugins/ping.py Normal file
View File

@ -0,0 +1,21 @@
import time
from pyrogram import Client, filters
from .. import config, help_dict, log_errors, public_log_errors
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.outgoing & filters.command(['ping', 'pong'], prefixes=config['config']['prefixes']))
@log_errors
@public_log_errors
async def ping_pong(client, message):
strings = {
'ping': 'Pong!',
'pong': 'Ping!'
}
text = strings[message.command[0].lower()]
start = time.time()
reply = await message.reply_text(text)
end = time.time()
await reply.edit_text(f'{text}\n<i>{round((end-start)*1000)}ms</i>')
help_dict['ping'] = ('Ping',
'''{prefix}ping - Pong!
{prefix}pong - Ping!''')

View File

@ -0,0 +1,15 @@
import os
import signal
from pyrogram import Client, filters
from .. import config, help_dict, log_errors, public_log_errors
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.outgoing & filters.command(['poweroff', 'shutdown', 'stop'], prefixes=config['config']['prefixes']))
@log_errors
@public_log_errors
async def poweroff(client, message):
await message.reply_text('Goodbye')
os.kill(os.getpid(), signal.SIGINT)
help_dict['poweroff'] = ('Poweroff',
'''{prefix}poweroff - Turns off the userbot
Aliases: {prefix}shutdown, {prefix}stop''')

View File

@ -0,0 +1,71 @@
# https://greentreesnakes.readthedocs.io/
import re
import ast
import sys
import html
import inspect
import asyncio
from io import StringIO
from pyrogram import Client, filters
from .. import config, help_dict, log_errors, slave, apps, session, public_log_errors
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.outgoing & filters.regex('^(?:' + '|'.join(map(re.escape, config['config']['prefixes'])) + r')exec\s+([\s\S]+)$'))
@log_errors
@public_log_errors
async def pyexec(client, message):
code = message.matches[0].group(1).strip()
class UniqueExecReturnIdentifier:
pass
tree = ast.parse(code)
obody = tree.body
body = obody.copy()
body.append(ast.Return(ast.Name('_ueri', ast.Load())))
def _gf(body):
# args: c, client, m, message, executing, r, reply, _ueri
func = ast.AsyncFunctionDef('ex', ast.arguments([], [ast.arg(i, None, None) for i in ['c', 'client', 'm', 'message', 'executing', 'r', 'reply', '_ueri']], None, [], [], None, []), body, [], None, None)
ast.fix_missing_locations(func)
mod = ast.parse('')
mod.body = [func]
fl = locals().copy()
exec(compile(mod, '<ast>', 'exec'), globals(), fl)
return fl['ex']
try:
exx = _gf(body)
except SyntaxError as ex:
if ex.msg != "'return' with value in async generator":
raise
exx = _gf(obody)
reply = await message.reply_text('Executing...')
async_obj = exx(client, client, message, message, reply, message.reply_to_message, message.reply_to_message, UniqueExecReturnIdentifier)
stdout = sys.stdout
stderr = sys.stderr
wrapped_stdout = StringIO()
wrapped_stderr = StringIO()
try:
sys.stdout = wrapped_stdout
sys.stderr = wrapped_stderr
if inspect.isasyncgen(async_obj):
returned = [i async for i in async_obj]
else:
returned = [await async_obj]
if returned == [UniqueExecReturnIdentifier]:
returned = []
finally:
sys.stdout = stdout
sys.stderr = stderr
wrapped_stderr.seek(0)
wrapped_stdout.seek(0)
output = ''
wrapped_stderr_text = wrapped_stderr.read().strip()
if wrapped_stderr_text:
output += f'<code>{html.escape(wrapped_stderr_text)}</code>\n'
wrapped_stdout_text = wrapped_stdout.read().strip()
if wrapped_stdout_text:
output += f'<code>{html.escape(wrapped_stdout_text)}</code>\n'
for i in returned:
output += f'<code>{html.escape(str(i).strip())}</code>\n'
if not output.strip():
output = 'Executed'
await reply.edit_text(output)
help_dict['pyexec'] = ('Exec', '{prefix}exec <i>&lt;python code&gt;</i> - Executes python code')

View File

@ -0,0 +1,28 @@
import re
import html
import asyncio
from pyrogram import Client, filters
from .. import config, help_dict, log_errors, public_log_errors
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.outgoing & filters.regex('^(?:' + '|'.join(map(re.escape, config['config']['prefixes'])) + r')(?:(?:ba)?sh|shell|term(?:inal)?)\s+(.+)(?:\n([\s\S]+))?$'))
@log_errors
@public_log_errors
async def shell(client, message):
command = message.matches[0].group(1)
stdin = message.matches[0].group(2)
reply = await message.reply_text('Executing...')
process = await asyncio.create_subprocess_shell(command, stdin=asyncio.subprocess.PIPE if stdin else None, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
stdout, stderr = await process.communicate(stdin.encode() if stdin else None)
returncode = process.returncode
text = f'<b>Exit Code:</b> <code>{returncode}</code>\n'
stdout = stdout.decode().replace('\r', '').strip('\n')
stderr = stderr.decode().replace('\r', '').strip('\n')
if stderr:
text += f'<code>{html.escape(stderr)}</code>\n'
if stdout:
text += f'<code>{html.escape(stdout)}</code>'
await reply.edit_text(text)
help_dict['shell'] = ('Shell',
'''{prefix}sh <i>&lt;command&gt;</i> \\n <i>[stdin]</i> - Executes <i>&lt;command&gt;</i> in shell
Aliases: {prefix}bash, {prefix}shell, {prefix}term, {prefix}terminal''')

View File

@ -0,0 +1,43 @@
import html
import googletrans
from pyrogram import Client, filters
from .. import config, help_dict, log_errors, public_log_errors
PROBLEM_CODES = set(i for i in googletrans.LANGUAGES if '-' in i)
translator = googletrans.Translator()
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.outgoing & filters.command(['tr', 'translate'], prefixes=config['config']['prefixes']))
@log_errors
@public_log_errors
async def translate(client, message):
reply = message.reply_to_message
if getattr(reply, 'empty', True):
await message.reply_text('Reply required')
return
text = reply.text or reply.caption
if not text:
await message.reply_text('Text required')
return
src_lang = 'auto'
dest_lang = 'en'
lang = ' '.join(message.command[1:]).lower()
for i in PROBLEM_CODES:
if lang.startswith(i):
src_lang = i
lang = lang[len(i):]
if lang:
dest_lang = lang[1:] or 'en'
break
else:
lang = lang.split('-', 1)
if len(lang) == 1:
dest_lang = lang.pop(0) or dest_lang
else:
src_lang, dest_lang = lang
def _translate():
return translator.translate(text, src=src_lang, dest=dest_lang)
await message.reply_text((await client.loop.run_in_executor(None, _translate)).text, parse_mode=None)
help_dict['translate'] = ('Translate',
'''{prefix}translate <i>(as reply to text)</i> <i>[src]-[dest]</i> - Translates text and stuff
Aliases: {prefix}tr''')

38
sukuinote/plugins/ud.py Normal file
View File

@ -0,0 +1,38 @@
import html
from pyrogram import Client, filters
from pyrogram.errors.exceptions.forbidden_403 import Forbidden
from .. import slave, config, help_dict, log_errors, public_log_errors
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.outgoing & filters.command(['ud', 'urbandictionary'], prefixes=config['config']['prefixes']))
@log_errors
@public_log_errors
async def ud(client, message):
bot = await slave.get_me()
query = message.command
page = 1
query.pop(0)
if query and query[0].isnumeric():
page = int(query.pop(0))
page -= 1
if page < 0:
page = 0
elif page > 9:
page = 9
query = ' '.join(query)
if not query:
return
results = await client.get_inline_bot_results(bot.username or bot.id, 'ud' + query)
if not results.results:
await message.reply_text('There are no definitions')
return
try:
await message.reply_inline_bot_result(results.query_id, results.results[page].id)
except IndexError:
await message.reply_text(f'There are only {len(results.results)} definitions')
except Forbidden:
await message.reply_text({'message': results.results[page].send_message.message, 'entities': results.results[page].send_message.entities}, disable_web_page_preview=True, parse_mode='through')
help_dict['ud'] = ('Urban Dictionary',
'''{prefix}urbandictionary <i>&lt;query&gt;</i> - Gets the definition of <i>&lt;query&gt;</i> via Urban Dictionary
Aliases: {prefix}ud
Can also be activated inline with: @{bot} urbandictionary <i>&lt;query&gt;</i> or @{bot} ud <i>&lt;query&gt;</i>''')

View File

@ -0,0 +1,52 @@
import html
import asyncio
from pyrogram import Client, ContinuePropagation
from pyrogram.raw.types import UpdateNewChannelMessage, UpdateNewMessage, MessageService, PeerChat, PeerChannel, MessageActionChatAddUser, MessageActionChatJoinedByLink
from .. import config, log_errors, slave
def sexy_user_name(user):
text = user.first_name
if user.last_name:
text += ' ' + user.last_name
return f'{"<code>[DELETED]</code>" if user.deleted else html.escape(text or "Empty???")} [<code>{user.id}</code>]'
handled = set()
lock = asyncio.Lock()
@Client.on_raw_update()
@log_errors
async def log_user_joins(client, update, users, chats):
if isinstance(update, (UpdateNewChannelMessage, UpdateNewMessage)):
message = update.message
if isinstance(message, MessageService):
action = message.action
if isinstance(action, (MessageActionChatAddUser, MessageActionChatJoinedByLink)):
if isinstance(message.to_id, PeerChannel):
chat_id = message.to_id.channel_id
sexy_chat_id = int('-100' + str(chat_id))
elif isinstance(message.to_id, PeerChat):
chat_id = message.to_id.chat_id
sexy_chat_id = -chat_id
else:
return
is_join = isinstance(action, MessageActionChatJoinedByLink)
if not is_join:
is_join = action.users == [message.from_id]
if is_join and not config['config']['log_user_joins']:
raise ContinuePropagation
if not is_join and not config['config']['log_user_adds']:
raise ContinuePropagation
text = f"<b>{'User Join Event' if is_join else 'User Add Event'}</b>\n- <b>Chat:</b> {html.escape(chats[chat_id].title)} [<code>{sexy_chat_id}</code>]\n"
async with lock:
if (sexy_chat_id, message.id) not in handled:
if is_join:
text += f'- <b>User:</b> {sexy_user_name(users[message.from_id])}\n'
if isinstance(action, MessageActionChatJoinedByLink):
text += f'- <b>Inviter:</b> {sexy_user_name(users[action.inviter_id])}'
else:
text += f'- <b>Adder:</b> {sexy_user_name(users[message.from_id])}\n- <b>Added Users:</b>\n'
for user in action.users:
text += f'--- {sexy_user_name(users[user])}\n'
await slave.send_message(config['config']['log_chat'], text)
handled.add((sexy_chat_id, message.id))
return
raise ContinuePropagation

View File

@ -0,0 +1,85 @@
import os
import time
import html
import asyncio
import datetime
import tempfile
from decimal import Decimal
from urllib.parse import quote as urlencode
from pyrogram import Client, filters
from .. import config, help_dict, log_errors, session, progress_callback, public_log_errors
@Client.on_message(~filters.sticker & ~filters.via_bot & ~filters.edited & filters.outgoing & filters.command(['trace', 'tracemoe', 'whatanime', 'wa', 'wait'], prefixes=config['config']['prefixes']))
@log_errors
@public_log_errors
async def whatanime(client, message):
media = message.photo or message.animation or message.video or message.sticker or message.document
if not media:
reply = message.reply_to_message
if not getattr(reply, 'empty', True):
media = reply.photo or reply.animation or reply.video or reply.sticker or reply.document
if not media:
await message.reply_text('Photo or GIF or Video or Sticker required')
return
with tempfile.TemporaryDirectory() as tempdir:
reply = await message.reply_text('Downloading...')
path = await client.download_media(media, file_name=os.path.join(tempdir, '0'), progress=progress_callback, progress_args=(reply, 'Downloading...', False))
new_path = os.path.join(tempdir, '1.png')
proc = await asyncio.create_subprocess_exec('ffmpeg', '-i', path, '-frames:v', '1', new_path)
await proc.communicate()
await reply.edit_text('Uploading...')
with open(new_path, 'rb') as file:
async with session.post('https://trace.moe/api/search', data={'image': file}) as resp:
json = await resp.json()
if isinstance(json, str):
await reply.edit_text(html.escape(json))
else:
try:
match = json['docs'][0]
except IndexError:
await reply.edit_text('No match')
else:
nsfw = match['is_adult']
title_native = match['title_native']
title_english = match['title_english']
title_romaji = match['title_romaji']
synonyms = ', '.join(match['synonyms'])
filename = match['filename']
tokenthumb = match['tokenthumb']
anilist_id = match['anilist_id']
episode = match['episode']
similarity = match['similarity']
from_time = str(datetime.timedelta(seconds=match['from'])).split('.', 1)[0].rjust(8, '0')
to_time = str(datetime.timedelta(seconds=match['to'])).split('.', 1)[0].rjust(8, '0')
at_time = match['at']
text = f'<a href="https://anilist.co/anime/{anilist_id}">{title_romaji}</a>'
if title_english:
text += f' ({title_english})'
if title_native:
text += f' ({title_native})'
if synonyms:
text += f'\n<b>Synonyms:</b> {synonyms}'
text += f'\n<b>Similarity:</b> {(Decimal(similarity) * 100).quantize(Decimal(".01"))}%\n'
if episode:
text += f'<b>Episode:</b> {episode}\n'
if nsfw:
text += '<b>Hentai/NSFW:</b> Yes'
async def _send_preview():
url = f'https://media.trace.moe/video/{anilist_id}/{urlencode(filename)}?t={at_time}&token={tokenthumb}'
with tempfile.NamedTemporaryFile() as file:
async with session.get(url) as resp:
while True:
chunk = await resp.content.read(10)
if not chunk:
break
file.write(chunk)
file.seek(0)
try:
await reply.reply_video(file.name, caption=f'{from_time} - {to_time}')
except Exception:
await reply.reply_text('Cannot send preview :/')
await asyncio.gather(reply.edit_text(text, disable_web_page_preview=True), _send_preview())
help_dict['whatanime'] = ('WhatAnime',
'''{prefix}whatanime <i>(as caption of Photo/GIF/Video/Sticker or reply)</i> - Reverse searches anime, thanks to trace.moe
Aliases: {prefix}trace, {prefix}tracemoe, {prefix}wa, {prefix}wait''')

View File

@ -0,0 +1,234 @@
import re
import time
import json
import asyncio
import datetime
from pyrogram import Client, filters
from pyrogram.parser import html as pyrogram_html
from pyrogram.types import InputTextMessageContent, InlineKeyboardMarkup, InlineKeyboardButton, InlineQueryResultArticle, InlineQueryResultPhoto, InputMediaPhoto
from .. import session, app_user_ids, log_errors
all_anilists = dict()
anilists_lock = asyncio.Lock()
MEDIA_QUERY = '''query ($id: Int, $search: String) {
Page (perPage: 10) {
media (id: $id, search: $search) {
id
title {
romaji
english
native
}
type
format
status
description(asHtml: true)
episodes
duration
chapters
volumes
genres
synonyms
averageScore
nextAiringEpisode {
airingAt
timeUntilAiring
}
siteUrl
}
}
}'''
FORMAT_NAMES = {
"TV": "TV",
"TV_SHORT": "TV Short",
"MOVIE": "Movie",
"SPECIAL": "Special",
"OVA": "OVA",
"ONA": "ONA",
"MUSIC": "Music",
"MANGA": "Manga",
"NOVEL": "Novel",
"ONE_SHOT": "One Shot"
}
CHARACTER_QUERY = '''query ($id: Int, $search: String) {
Page (perPage: 10) {
characters (id: $id, search: $search) {
name {
full
native
alternative
}
description(asHtml: true)
image {
large
}
siteUrl
}
}
}'''
async def generate_media(anilist):
title_romaji = anilist['title']['romaji']
title_english = anilist['title']['english']
title_native = anilist['title']['native']
type = anilist['type'].capitalize()
format = anilist['format']
format = FORMAT_NAMES.get(format, format)
status = anilist['status'].replace('_', ' ').title()
description = (anilist.get('description') or '').strip()
episodes = anilist['episodes']
duration = anilist['duration']
chapters = anilist['chapters']
volumes = anilist['volumes']
genres = ', '.join(anilist['genres'])
synonyms = ', '.join(anilist['synonyms'])
average_score = anilist['averageScore']
site_url = anilist['siteUrl']
text = f'<a href="{site_url}">{title_romaji}</a>'
if title_english:
text += f' ({title_english})'
if title_native:
text += f' ({title_native})'
if synonyms:
text += f'\n<b>Synonyms:</b> {synonyms}'
if genres:
text += f'\n<b>Genres:</b> {genres}'
text += f'\n<b>Type:</b> {type}\n'
if anilist['type'] != 'MANGA':
text += f'<b>Format:</b> {format}\n'
text += f'<b>Status:</b> {status}\n'
if anilist['nextAiringEpisode']:
airing_at = str(datetime.datetime.fromtimestamp(anilist['nextAiringEpisode']['airingAt']))
time_until_airing = str(datetime.timedelta(seconds=anilist['nextAiringEpisode']['timeUntilAiring']))
text += f'<b>Airing At:</b> {airing_at}\n<b>Airing In:</b> {time_until_airing}\n'
if average_score is not None:
text += f'<b>Average Score:</b> {average_score}%\n'
if episodes:
text += f'<b>Episodes:</b> {episodes}\n'
if duration:
text += f'<b>Duration:</b> {duration} minutes per episode\n'
if chapters:
text += f'<b>Chapters:</b> {chapters}\n'
if volumes:
text += f'<b>Volumes:</b> {volumes}\n'
if description:
text += '<b>Description:</b>\n'
parser = pyrogram_html.HTML(None)
total_length = len((await parser.parse(text))['message'])
if len(description) > 1023-total_length:
description = description[:1022-total_length] + ''
text += description
return text, f"https://img.anili.st/media/{anilist['id']}"
async def generate_character(anilist):
title_full = anilist['name']['full']
title_native = anilist['name']['native']
title_alternative = ', '.join(anilist['name']['alternative'])
description = (anilist['description'] or '').strip()
site_url = anilist['siteUrl']
image = anilist['image']['large']
text = f'<a href="{site_url}">{title_full}</a>'
if title_native:
text += f' ({title_native})'
if title_alternative:
text += f'\n<b>Synonyms:</b> {title_alternative}'
if description:
text += '\n'
parser = pyrogram_html.HTML(None)
total_length = len((await parser.parse(text))['message'])
if len(description) > 1023-total_length:
description = description[:1022-total_length] + ''
text += description
return text, image
@Client.on_inline_query(filters.regex(r'^a(?:ni)?l(?:ist)?(c(?:har(?:acter)?)?)?\s+(.+)$'))
@log_errors
async def anilist_query(client, inline_query):
if inline_query.from_user.id not in app_user_ids:
await inline_query.answer([
InlineQueryResultArticle('...no', InputTextMessageContent('...no'))
], cache_time=3600, is_personal=True)
return
character = bool(inline_query.matches[0].group(1))
query = inline_query.matches[0].group(2).strip().lower()
async with anilists_lock:
if (character, query) not in all_anilists:
async with session.post('https://graphql.anilist.co', data=json.dumps({'query': CHARACTER_QUERY if character else MEDIA_QUERY, 'variables': {'search': query}}), headers={'Content-Type': 'application/json', 'Accept': 'application/json'}) as resp:
all_anilists[(character, query)] = (await resp.json())['data']['Page']['characters' if character else 'media']
anilists = all_anilists[(character, query)]
answers = []
parser = pyrogram_html.HTML(client)
for a, anilist in enumerate(anilists):
text, image = await (generate_character if character else generate_media)(anilist)
buttons = [InlineKeyboardButton('Back', 'anilist_back'), InlineKeyboardButton(f'{a + 1}/{len(anilists)}', 'anilist_nop'), InlineKeyboardButton('Next', 'anilist_next')]
if not a:
buttons.pop(0)
if len(anilists) == a + 1:
buttons.pop()
split = text.split('\n', 1)
title = (await parser.parse(split[0]))['message']
try:
description = (await parser.parse(split[1]))['message']
except IndexError:
description = None
answers.append(InlineQueryResultPhoto(image, title=title, description=description, caption=text, reply_markup=InlineKeyboardMarkup([buttons]), id=f'anilist{a}-{time.time()}'))
await inline_query.answer(answers, is_personal=True, is_gallery=False)
@Client.on_callback_query(filters.regex('^anilist_nop$'))
@log_errors
async def anilist_nop(client, callback_query):
await callback_query.answer(cache_time=3600)
message_info = dict()
message_lock = asyncio.Lock()
@Client.on_chosen_inline_result()
@log_errors
async def anilist_chosen(client, inline_result):
if inline_result.result_id.startswith('anilist'):
match = re.match(r'^a(?:ni)?l(?:ist)?(c(?:har(?:acter)?)?)?\s+(.+)$', inline_result.query)
if match:
character = bool(match.group(1))
query = match.group(2).strip().lower()
if query:
page = int(inline_result.result_id[7])
message_info[inline_result.inline_message_id] = query, page, character
async with anilists_lock:
if (character, query) not in all_anilists:
async with session.post('https://graphql.anilist.co', data=json.dumps({'query': CHARACTER_QUERY if character else MEDIA_QUERY, 'variables': {'search': query, 'page': 1, 'perPage': 10}}), headers={'Content-Type': 'application/json', 'Accept': 'application/json'}) as resp:
all_anilists[(character, query)] = (await resp.json())['data']['Page']['characters' if character else 'media']
return
inline_result.continue_propagation()
@Client.on_callback_query(filters.regex('^anilist_(back|next)$'))
@log_errors
async def anilist_move(client, callback_query):
if callback_query.from_user.id not in app_user_ids:
await callback_query.answer('...no', cache_time=3600, show_alert=True)
return
async with message_lock:
if callback_query.inline_message_id not in message_info:
await callback_query.answer('This message is too old', cache_time=3600, show_alert=True)
return
query, page, character = message_info[callback_query.inline_message_id]
opage = page
if callback_query.matches[0].group(1) == 'back':
page -= 1
elif callback_query.matches[0].group(1) == 'next':
page += 1
if page < 0:
page = 0
elif page > 9:
page = 9
if page != opage:
async with anilists_lock:
anilists = all_anilists[(character, query)]
text, image = await (generate_character if character else generate_media)(anilists[page])
buttons = [InlineKeyboardButton('Back', 'anilist_back'), InlineKeyboardButton(f'{page + 1}/{len(anilists)}', 'anilist_nop'), InlineKeyboardButton('Next', 'anilist_next')]
if not page:
buttons.pop(0)
if len(anilists) == page + 1:
buttons.pop()
await callback_query.edit_message_media(InputMediaPhoto(image, caption=text), reply_markup=InlineKeyboardMarkup([buttons]))
message_info[callback_query.inline_message_id] = query, page, character
await callback_query.answer()

View File

@ -0,0 +1,112 @@
import time
import html
import asyncio
from pyrogram import Client, filters
from pyrogram.parser import html as pyrogram_html
from pyrogram.types import InlineQueryResultArticle, InputTextMessageContent, InlineKeyboardMarkup, InlineKeyboardButton
from .. import config, help_dict, app_user_ids, log_errors
@Client.on_inline_query(filters.regex('^help$'))
@log_errors
async def main_help(client, inline_query):
if inline_query.from_user.id not in app_user_ids:
await inline_query.answer([
InlineQueryResultArticle('...no', InputTextMessageContent('...no'))
], cache_time=3600, is_personal=True)
return
buttons = []
to_append = []
prefixes = config['config']['prefixes'] or []
if not isinstance(prefixes, list):
prefixes = prefixes.split()
prefixes = ', '.join(prefixes)
prefix = prefixes[0]
results = []
parser = pyrogram_html.HTML(client)
me = None
for internal_name in help_dict:
external_name, help_text = help_dict[internal_name]
if '{bot}' in help_text:
if not me:
me = await client.get_me()
text = f'Help for {html.escape(external_name)}:\nAvaliable prefixes: {prefixes}\n\n{help_text.format(prefix=prefix, bot=getattr(me, "username", None))}'
to_append.append(InlineKeyboardButton(external_name, f'help_m{internal_name}'))
if len(to_append) > 2:
buttons.append(to_append)
to_append = []
results.append(InlineQueryResultArticle(external_name, InputTextMessageContent(text), reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton('Back', 'help_back')]]), description=(await parser.parse(help_text.format(prefix=prefix, bot=getattr(me, 'username', None))))['message'], id=f'helpm{internal_name}-{time.time()}'))
else:
if to_append:
buttons.append(to_append)
results.insert(0, InlineQueryResultArticle('Main Menu', InputTextMessageContent('Select the plugin you want help with'), reply_markup=InlineKeyboardMarkup(buttons), id=f'helpa-{time.time()}'))
await inline_query.answer(results, is_personal=True)
message_info = dict()
lock = asyncio.Lock()
@Client.on_chosen_inline_result()
@log_errors
async def help_chosen(client, inline_result):
if inline_result.query == 'help':
if inline_result.result_id.startswith('helpm'):
location = inline_result.result_id[5:].split('-')
location.pop()
message_info[inline_result.inline_message_id] = '-'.join(location)
return
elif inline_result.result_id.startswith('helpa'):
message_info[inline_result.inline_message_id] = None
return
inline_result.continue_propagation()
@Client.on_callback_query(filters.regex('^help_back$'))
@log_errors
async def help_back(client, callback_query):
if callback_query.from_user.id not in app_user_ids:
await callback_query.answer('...no', cache_time=3600, show_alert=True)
return
message_identifier = callback_query.inline_message_id
async with lock:
if message_info.get(message_identifier, True):
buttons = []
to_append = []
for internal_name in help_dict:
external_name, _ = help_dict[internal_name]
to_append.append(InlineKeyboardButton(external_name, f'help_m{internal_name}'))
if len(to_append) > 2:
buttons.append(to_append)
to_append = []
if to_append:
buttons.append(to_append)
await callback_query.edit_message_text('Select the plugin you want help with', reply_markup=InlineKeyboardMarkup(buttons))
message_info[message_identifier] = None
await callback_query.answer()
@Client.on_callback_query(filters.regex('^help_m(.+)$'))
@log_errors
async def help_m(client, callback_query):
if callback_query.from_user.id not in app_user_ids:
await callback_query.answer('...no', cache_time=3600, show_alert=True)
return
message_identifier = callback_query.inline_message_id
plugin = callback_query.matches[0].group(1)
async with lock:
if message_info.get(message_identifier) != plugin:
if plugin not in help_dict:
await callback_query.answer('What plugin?', cache_time=3600, show_alert=True)
return
external_name, help_text = help_dict[plugin]
prefixes = config['config']['prefixes'] or []
if not isinstance(prefixes, list):
prefixes = prefixes.split()
prefixes = ', '.join(prefixes)
text = f'Help for {html.escape(external_name)}:\n'
prefix = ''
if prefixes:
text += f'Avaliable prefixes: {prefixes}\n'
prefix = prefixes[0]
me = None
if '{bot}' in help_text:
me = await client.get_me()
text += f'\n{help_text.format(prefix=prefix, bot=getattr(me, "username", None))}'
await callback_query.edit_message_text(text, reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton('Back', 'help_back')]]))
message_info[message_identifier] = plugin
await callback_query.answer()

View File

@ -0,0 +1,33 @@
import os
import requests
from pyrogram import Client, filters
from pyrogram.types import InputTextMessageContent, InlineQueryResultArticle, InlineQueryResultPhoto, InlineQueryResultAnimation
from .. import log_errors, session, app_user_ids
resp = requests.get('https://nekos.life/api/v2/endpoints')
json = resp.json()
for i in json:
_, i = i.split(' ', 1)
i = i.strip()
if i.startswith('/api/v2/img/<\''):
for i in os.path.basename(i)[1:-1].split(', '):
i = i[1:-1]
if 'v3' in i:
continue
def _generate(i):
@Client.on_inline_query(filters.regex(f'^{i}$'))
@log_errors
async def func(client, inline_query):
if inline_query.from_user.id not in app_user_ids:
await inline_query.answer([InlineQueryResultArticle('...no', InputTextMessageContent('...no'))], cache_time=3600, is_personal=True)
return
async with session.get(f'https://nekos.life/api/v2/img/{i}') as resp:
url = (await resp.json())['url']
call = InlineQueryResultAnimation if '.gif' == os.path.splitext(url)[1] else InlineQueryResultPhoto
await inline_query.answer([call(url, caption=url, parse_mode=None)], cache_time=0)
return func
func = _generate(i)
globals()[i] = func
locals()[i] = func
func = None
break

View File

@ -0,0 +1,101 @@
import re
import time
import html
import asyncio
from urllib.parse import quote as urlencode
from pyrogram import Client, filters
from pyrogram.types import InlineQueryResultArticle, InputTextMessageContent, InlineKeyboardMarkup, InlineKeyboardButton
from .. import session, app_user_ids, log_errors
all_definitions = dict()
definitions_lock = asyncio.Lock()
@Client.on_inline_query(filters.regex('^u(?:rban)?d(?:ictionary)?(.+)$'))
@log_errors
async def ud(client, inline_query):
if inline_query.from_user.id not in app_user_ids:
await inline_query.answer([
InlineQueryResultArticle('...no', InputTextMessageContent('...no'))
], cache_time=3600, is_personal=True)
return
query = inline_query.matches[0].group(1).strip().lower()
async with definitions_lock:
if query not in all_definitions:
async with session.get(f'https://api.urbandictionary.com/v0/define?term={urlencode(query)}') as resp:
all_definitions[query] = (await resp.json())['list']
definitions = all_definitions[query]
answers = []
for a, definition in enumerate(definitions):
text = f'''<a href="{definition["permalink"]}">{html.escape(definition["word"])}</a>
<b>Definition:</b>
{html.escape(definition["definition"])}'''
if definition['example']:
text += f'\n<b>Examples:</b>\n{html.escape(definition["example"])}'
buttons = [InlineKeyboardButton('Back', 'ud_back'), InlineKeyboardButton(f'{a + 1}/{len(definitions)}', 'ud_nop'), InlineKeyboardButton('Next', 'ud_next')]
if not a:
buttons.pop(0)
if len(definitions) == a + 1:
buttons.pop()
answers.append(InlineQueryResultArticle(definition['word'], InputTextMessageContent(text, disable_web_page_preview=True), reply_markup=InlineKeyboardMarkup([buttons]), id=f'ud{a}-{time.time()}', description=definition['definition']))
await inline_query.answer(answers, is_personal=True)
@Client.on_callback_query(filters.regex('^ud_nop$'))
@log_errors
async def ud_nop(client, callback_query):
await callback_query.answer(cache_time=3600)
message_info = dict()
message_lock = asyncio.Lock()
@Client.on_chosen_inline_result()
@log_errors
async def ud_chosen(client, inline_result):
if inline_result.result_id.startswith('ud'):
match = re.match('^u(?:rban)?d(?:dictionary)?(.*)$', inline_result.query)
if match:
query = match.group(1).strip().lower()
if query:
page = int(inline_result.result_id[2])
message_info[inline_result.inline_message_id] = query, page
async with definitions_lock:
if query not in all_definitions:
async with session.get(f'https://api.urbandictionary.com/v0/define?term={urlencode(query)}') as resp:
all_definitions[query] = (await resp.json())['list']
return
inline_result.continue_propagation()
@Client.on_callback_query(filters.regex('^ud_(back|next)$'))
@log_errors
async def ud_move(client, callback_query):
if callback_query.from_user.id not in app_user_ids:
await callback_query.answer('...no', cache_time=3600, show_alert=True)
return
async with message_lock:
if callback_query.inline_message_id not in message_info:
await callback_query.answer('This message is too old', cache_time=3600, show_alert=True)
return
query, page = message_info[callback_query.inline_message_id]
opage = page
if callback_query.matches[0].group(1) == 'back':
page -= 1
elif callback_query.matches[0].group(1) == 'next':
page += 1
if page < 0:
page = 0
elif page > 9:
page = 9
if page != opage:
async with definitions_lock:
definitions = all_definitions[query]
definition = definitions[page]
text = f'''<a href="{definition["permalink"]}">{html.escape(definition["word"])}</a>
<b>Definition:</b>
{html.escape(definition["definition"])}'''
if definition['example']:
text += f'\n<b>Examples:</b>\n{html.escape(definition["example"])}'
buttons = [InlineKeyboardButton('Back', 'ud_back'), InlineKeyboardButton(f'{page + 1}/{len(definitions)}', 'ud_nop'), InlineKeyboardButton('Next', 'ud_next')]
if not page:
buttons.pop(0)
if len(definitions) == page + 1:
buttons.pop()
await callback_query.edit_message_text(text, disable_web_page_preview=True, reply_markup=InlineKeyboardMarkup([buttons]))
message_info[callback_query.inline_message_id] = query, page
await callback_query.answer()