diff --git a/stdplugins/fixreply.py b/stdplugins/fixreply.py new file mode 100644 index 0000000..997ad6e --- /dev/null +++ b/stdplugins/fixreply.py @@ -0,0 +1,27 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +import asyncio + +from telethon import events + + +_last_messages = {} + + +@borg.on(events.NewMessage(outgoing=True)) +async def _(event): + _last_messages[event.chat_id] = event.message + + +@borg.on(events.NewMessage(pattern=r"\.(fix)?reply", outgoing=True)) +async def _(event): + if not event.is_reply or event.chat_id not in _last_messages: + return + + message = _last_messages[event.chat_id] + chat = await event.get_input_chat() + await asyncio.wait([ + borg.delete_messages(chat, [event.id, message.id]), + borg.send_message(chat, message, reply_to=event.reply_to_msg_id) + ]) diff --git a/stdplugins/fpost.py b/stdplugins/fpost.py new file mode 100644 index 0000000..b587acd --- /dev/null +++ b/stdplugins/fpost.py @@ -0,0 +1,27 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import string + +from telethon import events +from telethon.tl import types + +msg_cache = {} + + +@borg.on(events.NewMessage(pattern=r"\.fpost\s+(.*)", outgoing=True)) +async def _(event): + await event.delete() + text = event.pattern_match.group(1) + destination = await event.get_input_chat() + + for c in text.lower(): + if c not in string.ascii_lowercase: + continue + if c not in msg_cache: + async for msg in borg.iter_messages(None, search=c): + if msg.raw_text.lower() == c and msg.media is None: + msg_cache[c] = msg + break + await borg.forward_messages(destination, msg_cache[c]) diff --git a/stdplugins/markdown.py b/stdplugins/markdown.py index b56e7ed..6fe940c 100644 --- a/stdplugins/markdown.py +++ b/stdplugins/markdown.py @@ -47,6 +47,10 @@ def parse_strikethrough(m): return ("\u0336".join(m[1]) + "\u0336"), None +def parse_enclosing_circle(m): + return ("\u20e0".join(m[1]) + "\u20e0"), None + + def parse_subreddit(m): text = '/' + m.group(3) entity = MessageEntityTextUrl( @@ -82,6 +86,7 @@ MATCHERS = [ (get_tag_parser('`', MessageEntityCode)), (re.compile(r'\+\+(.+?)\+\+'), parse_aesthetics), (re.compile(r'~~(.+?)~~'), parse_strikethrough), + (re.compile(r'@@(.+?)@@'), parse_enclosing_circle), (re.compile(r'([^/\w]|^)(/?(r/\w+))'), parse_subreddit), (re.compile(r'(!\w+)'), parse_snip) ] diff --git a/stdplugins/randomsticker.py b/stdplugins/randomsticker.py new file mode 100644 index 0000000..b48ed8d --- /dev/null +++ b/stdplugins/randomsticker.py @@ -0,0 +1,43 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import random + +from telethon import events, types, functions, utils + + +def choser(cmd, pack, blacklist={}): + docs = None + @borg.on(events.NewMessage(pattern=rf'\.{cmd}', outgoing=True)) + async def handler(event): + await event.delete() + + nonlocal docs + if docs is None: + docs = [ + utils.get_input_document(x) + for x in (await borg(functions.messages.GetStickerSetRequest(types.InputStickerSetShortName(pack)))).documents + if x.id not in blacklist + ] + + await event.respond(file=random.choice(docs)) + + +choser('brain', 'supermind') +choser('dab', 'DabOnHaters', { + 1653974154589768377, + 1653974154589768312, + 1653974154589767857, + 1653974154589768311, + 1653974154589767816, + 1653974154589767939, + 1653974154589767944, + 1653974154589767912, + 1653974154589767911, + 1653974154589767910, + 1653974154589767909, + 1653974154589767863, + 1653974154589767852, + 1653974154589768677 +}) \ No newline at end of file diff --git a/stdplugins/sed.py b/stdplugins/sed.py index 04f7320..78d8f9d 100644 --- a/stdplugins/sed.py +++ b/stdplugins/sed.py @@ -5,6 +5,8 @@ import regex from telethon import events, utils from telethon.tl import types, functions +from uniborg import util + HEADER = "「sed」\n" KNOWN_RE_BOTS = re.compile( r'(regex|moku|BananaButler_|rgx|l4mR)bot', @@ -17,6 +19,7 @@ KNOWN_RE_BOTS = re.compile( last_msgs = defaultdict(lambda: deque(maxlen=10)) +@util.sync_timeout(1) def doit(chat_id, match, original): fr = match.group(1) to = match.group(2) @@ -100,7 +103,7 @@ async def on_regex(event): if m is not None: s = f"{HEADER}{s}" out = await borg.send_message( - await event.get_input_chat(), s, reply_to=m.id + await event.get_input_chat(), s, reply_to=m.id, parse_mode=None ) last_msgs[chat_id].appendleft(out) elif s is not None: diff --git a/stdplugins/snip.py b/stdplugins/snip.py index d78c645..afd3b14 100644 --- a/stdplugins/snip.py +++ b/stdplugins/snip.py @@ -16,7 +16,7 @@ TYPE_DOCUMENT = 2 snips = storage.snips or {} -@borg.on(events.NewMessage(pattern=r'\.snip (\S+)', outgoing=True)) +@borg.on(events.NewMessage(pattern=r'(?:\.snip +|!)(\w+)$', outgoing=True)) async def on_snip(event): loop.create_task(event.delete()) name = event.pattern_match.group(1) diff --git a/stdplugins/who.py b/stdplugins/who.py index e4f5349..3982344 100644 --- a/stdplugins/who.py +++ b/stdplugins/who.py @@ -1,12 +1,21 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. +import html from telethon import events from telethon import utils from telethon.tl import types +def get_who_string(who): + who_string = html.escape(utils.get_display_name(who)) + if isinstance(who, (types.User, types.Channel)) and who.username: + who_string += f" (@{who.username})" + who_string += f", #{who.id}" + return who_string + + @borg.on(events.NewMessage(pattern=r"\.who", outgoing=True)) async def _(event): if not event.message.is_reply: @@ -14,14 +23,30 @@ async def _(event): else: msg = await event.message.get_reply_message() if msg.forward: + # FIXME forward privacy memes who = await borg.get_entity( msg.forward.from_id or msg.forward.channel_id) else: who = await msg.get_sender() - who_string = utils.get_display_name(who) - if isinstance(who, (types.User, types.Channel)) and who.username: - who_string += f" (@{who.username})" - who_string += f", [#{who.id}](tg://user?id={who.id})" + await event.edit(get_who_string(who), parse_mode='html') - await event.edit(who_string) + +@borg.on(events.NewMessage(pattern=r"\.members", outgoing=True)) +async def _(event): + members = [] + async for member in borg.iter_participants(event.chat_id): + messages = await borg.get_messages( + event.chat_id, + from_user=member, + limit=0 + ) + members.append(( + messages.total, + f"{messages.total} - {get_who_string(member)}" + )) + members = ( + m[1] for m in sorted(members, key=lambda m: m[0], reverse=True) + ) + + await event.edit("\n".join(members), parse_mode='html') diff --git a/uniborg/_core.py b/uniborg/_core.py index 7475028..43d8bd1 100644 --- a/uniborg/_core.py +++ b/uniborg/_core.py @@ -17,7 +17,7 @@ async def load_reload(event): try: if shortname in borg._plugins: - borg.remove_plugin(shortname) + await borg.remove_plugin(shortname) borg.load_plugin(shortname) msg = await event.respond( @@ -39,7 +39,7 @@ async def remove(event): if shortname == "_core": msg = await event.respond(f"Not removing {shortname}") elif shortname in borg._plugins: - borg.remove_plugin(shortname) + await borg.remove_plugin(shortname) msg = await event.respond(f"Removed plugin {shortname}") else: msg = await event.respond(f"Plugin {shortname} is not loaded") diff --git a/uniborg/uniborg.py b/uniborg/uniborg.py index 97465f1..3dd3447 100644 --- a/uniborg/uniborg.py +++ b/uniborg/uniborg.py @@ -3,6 +3,7 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. import asyncio import importlib.util +import inspect import logging from pathlib import Path @@ -71,7 +72,7 @@ class Uniborg(TelegramClient): self._plugins[shortname] = mod self._logger.info(f"Successfully loaded plugin {shortname}") - def remove_plugin(self, shortname): + async def remove_plugin(self, shortname): name = self._plugins[shortname].__name__ for i in reversed(range(len(self._event_builders))): @@ -79,7 +80,16 @@ class Uniborg(TelegramClient): if cb.__module__ == name: del self._event_builders[i] - del self._plugins[shortname] + plugin = self._plugins.pop(shortname) + if callable(getattr(plugin, 'unload', None)): + try: + unload = plugin.unload() + if inspect.isawaitable(unload): + await unload + except Exception: + self._logger.exception(f'Unhandled exception unloading {shortname}') + + del plugin self._logger.info(f"Removed plugin {shortname}") def await_event(self, event_matcher, filter=None): diff --git a/uniborg/util.py b/uniborg/util.py index 39cfc8c..1b93b26 100644 --- a/uniborg/util.py +++ b/uniborg/util.py @@ -2,7 +2,9 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. +import functools import re +import signal from telethon import events from telethon.tl.functions.messages import GetPeerDialogsRequest @@ -28,3 +30,20 @@ async def is_read(borg, entity, message, is_out=None): dialog = (await borg(GetPeerDialogsRequest([entity]))).dialogs[0] max_id = dialog.read_outbox_max_id if is_out else dialog.read_inbox_max_id return message_id <= max_id + +def _handle_timeout(signum, frame): + raise TimeoutError("Execution took too long") + +def sync_timeout(seconds): + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + signal.signal(signal.SIGALRM, _handle_timeout) + signal.setitimer(signal.ITIMER_REAL, seconds) + try: + r = func(*args, **kwargs) + finally: + signal.alarm(0) + return r + return wrapper + return decorator