Compare commits

..

3 Commits

Author SHA1 Message Date
udf 27d7b5ed82
Merge branch 'master' of https://git.togrand.xyz/uniborg/uniborg 2019-09-09 09:35:39 +02:00
udf 573f36bf57
fix broken indentation when lots of nested items are present 2019-05-19 23:00:23 +02:00
udf b21f5ec104
Fix trailing whitespaces
kate you're better than this
2019-05-19 22:59:43 +02:00
19 changed files with 92 additions and 666 deletions

View File

@ -1 +0,0 @@
../stdplugins/sed.py

View File

@ -1 +0,0 @@
../stdplugins/tl.py

View File

@ -1,18 +0,0 @@
# 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 logging
from uniborg import Uniborg
logging.basicConfig(level=logging.INFO)
borg = Uniborg(
"stdbot",
plugin_path="botplugins",
admins=[12345],
connection_retries=None
)
borg.run_until_disconnected()

View File

@ -1,9 +1,7 @@
# 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/.
"""
Mentions .all people in the group
"""
from telethon import events

View File

@ -1,48 +1,23 @@
# 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/.
"""
Counts how long a reply .chain is
"""
from collections import defaultdict
from telethon import events
from telethon.tl.functions.messages import SaveDraftRequest
def intify(d):
for k, v in d.items():
if isinstance(v, dict):
v = dict(intify(v))
yield int(k), v
global_cache = defaultdict(lambda: {}, intify(storage.cache or {}))
@borg.on(events.NewMessage(pattern=r"\.chain", outgoing=True))
async def _(event):
cache = global_cache[event.chat_id]
message = event.reply_to_msg_id
count = 0
while message is not None:
reply = cache.get(message, -1)
if reply == -1:
reply = None
if m := await borg.get_messages(event.chat_id, ids=message):
reply = m.reply_to_msg_id
cache[message] = reply
if len(cache) % 10 == 0:
await event.edit(f"Counting... ({len(cache)} cached entries)")
await event.edit("Counting...")
count = -1
message = event.message
while message:
reply = await message.get_reply_message()
if reply is None:
await borg(SaveDraftRequest(
await event.get_input_chat(),
"",
reply_to_msg_id=message
reply_to_msg_id=message.id
))
message = reply
count += 1
if count:
storage.cache = global_cache
await event.edit(f"Chain length: {count}")

View File

@ -1,9 +1,6 @@
# 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/.
"""
Reply with .fixreply to adjust your last message's reply
"""
import asyncio
from telethon import events

View File

@ -1,9 +1,7 @@
# 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/.
"""
.fpost a message with single-letter forwards
"""
import string
from telethon import events

View File

@ -1,9 +1,7 @@
# 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/.
"""
Show all .info about the replied message
"""
from telethon import events
from telethon.utils import add_surrogate
from telethon.tl.types import MessageEntityPre

View File

@ -1,9 +1,6 @@
# 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/.
"""
Enhances your markdown capabilities
"""
import re
from functools import partial
@ -96,9 +93,6 @@ MATCHERS = [
def parse(message, old_entities=None):
if not message:
return message
entities = []
old_entities = sorted(old_entities or [], key=lambda e: e.offset)

View File

@ -1,26 +0,0 @@
# 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/.
"""
Auto-fix audio files sent as voice-notes
"""
from telethon import events
@borg.on(events.NewMessage(outgoing=True))
async def _(e):
if e.fwd_from or e.via_bot_id:
return
if e.voice:
f = e.file
if f.title and f.performer:
caption = f"{f.performer} - {f.title}"
elif f.title:
caption = f.title
elif f.name:
caption = f.name
else:
caption = None
if caption:
await e.edit(caption)

View File

@ -1,9 +1,7 @@
# 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/.
"""
Perform ninja deletions or edits
"""
import asyncio
from telethon import events
@ -39,8 +37,8 @@ async def await_read(chat, message):
await fut
@borg.on(events.NewMessage(outgoing=True, pattern=r"^\.(del)(?:ete)?$")
@borg.on(events.NewMessage(outgoing=True, pattern=r"^\.(edit)(?:\s+(.*))?$")
@borg.on(util.admin_cmd(r"^\.(del)(?:ete)?$"))
@borg.on(util.admin_cmd(r"^\.(edit)(?:\s+(.*))?$"))
async def delete(event):
await event.delete()
command = event.pattern_match.group(1)

View File

@ -1,9 +1,7 @@
# 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/.
"""
Send a random .brain or .dab sticker
"""
import random
from telethon import events, types, functions, utils
@ -23,7 +21,7 @@ def choser(cmd, pack, blacklist={}):
if x.id not in blacklist
]
await event.respond(file=random.choice(docs), reply_to=event.reply_to_msg_id)
await event.respond(file=random.choice(docs))
choser('brain', 'supermind')
@ -42,8 +40,4 @@ choser('dab', 'DabOnHaters', {
1653974154589767863,
1653974154589767852,
1653974154589768677
})
choser('fp', 'facepalmstickers', {
285892071401720411,
285892071401725809
})
})

View File

@ -1,20 +1,15 @@
"""
Become @regexbot when the bot is missing
"""
import regex as re
from collections import defaultdict, deque
import re
import regex
from telethon import events, utils
from telethon.tl import types, functions
from uniborg import util
SED_PATTERN = r'^s/((?:\\/|[^/])+)/((?:\\/|[^/])*)(/.*)?'
GROUP0_RE = re.compile(r'(?<!\\)((?:\\\\)*)\\0')
HEADER = '' if borg.me.bot else '「sed」\n'
HEADER = "「sed」\n"
KNOWN_RE_BOTS = re.compile(
r'(regex|moku|ou|BananaButler_|rgx|l4mR|ProgrammingAndGirls)bot',
r'(regex|moku|BananaButler_|rgx|l4mR)bot',
flags=re.IGNORECASE
)
@ -24,20 +19,11 @@ KNOWN_RE_BOTS = re.compile(
last_msgs = defaultdict(lambda: deque(maxlen=10))
def cleanup_pattern(match):
from_ = match.group(1)
@util.sync_timeout(1)
def doit(chat_id, match, original):
fr = match.group(1)
to = match.group(2)
to = to.replace('\\/', '/')
to = GROUP0_RE.sub(r'\1\\g<0>', to)
return from_, to
#@util.sync_timeout(1)
async def doit(message, match):
fr, to = cleanup_pattern(match)
try:
fl = match.group(3)
if fl is None:
@ -49,49 +35,34 @@ async def doit(message, match):
# Build Python regex flags
count = 1
flags = 0
for f in fl.lower():
for f in fl:
if f == 'i':
flags |= re.IGNORECASE
elif f == 'm':
flags |= re.MULTILINE
elif f == 's':
flags |= re.DOTALL
flags |= regex.IGNORECASE
elif f == 'g':
count = 0
elif f == 'x':
flags |= re.VERBOSE
else:
await message.reply(f'{HEADER}Unknown flag: {f}')
return
return None, f"Unknown flag: {f}"
def substitute(m):
if s := m.raw_text:
def actually_doit(original):
try:
s = original.message
if s.startswith(HEADER):
s = s[len(HEADER):]
else:
return None
s, i = regex.subn(fr, to, s, count=count, flags=flags)
if i > 0:
return original, s
except Exception as e:
return None, f"u dun goofed m8: {str(e)}"
return None, None
s, i = re.subn(fr, to, s, count=count, flags=flags)
if i > 0:
return s
try:
msg = None
substitution = None
if message.is_reply:
msg = await message.get_reply_message()
substitution = substitute(msg)
else:
for msg in reversed(last_msgs[message.chat_id]):
substitution = substitute(msg)
if substitution is not None:
break # msg is also set
if substitution is not None:
return await msg.reply(f'{HEADER}{substitution}', parse_mode=None)
except Exception as e:
await message.reply(f'{HEADER}fuck me: {e}')
if original is not None:
return actually_doit(original)
# Try matching the last few messages
for original in last_msgs[chat_id]:
m, s = actually_doit(original)
if s is not None:
return m, s
return None, None
async def group_has_sedbot(group):
@ -104,34 +75,38 @@ async def group_has_sedbot(group):
return any(KNOWN_RE_BOTS.match(x.username or '') for x in full.users)
async def sed(event):
if event.fwd_from:
return
if not (borg.me.bot or event.is_private):
if not event.out:
return
if await group_has_sedbot(await event.get_input_chat()):
return
message = await doit(event.message, event.pattern_match)
if message:
last_msgs[event.chat_id].append(message)
# Don't save sed commands or we would be able to sed those
raise events.StopPropagation
@borg.on(events.NewMessage)
async def catch_all(event):
last_msgs[event.chat_id].append(event.message)
async def on_message(event):
last_msgs[event.chat_id].appendleft(event.message)
@borg.on(events.MessageEdited)
async def catch_edit(event):
for i, message in enumerate(last_msgs[event.chat_id]):
if message.id == event.id:
last_msgs[event.chat_id][i] = event.message
async def on_edit(event):
for m in last_msgs[event.chat_id]:
if m.id == event.id:
m.raw_text = event.raw_text
break
@borg.on(events.NewMessage(
pattern=re.compile(r"^s/((?:\\/|[^/])+)/((?:\\/|[^/])*)(/.*)?")))
async def on_regex(event):
if event.fwd_from:
return
if not event.is_private and\
await group_has_sedbot(await event.get_input_chat()):
return
borg.on(events.NewMessage(pattern=SED_PATTERN))(sed)
borg.on(events.MessageEdited(pattern=SED_PATTERN))(sed)
chat_id = utils.get_peer_id(await event.get_input_chat())
m, s = doit(chat_id, event.pattern_match, await event.get_reply_message())
if m is not None:
s = f"{HEADER}{s}"
out = await borg.send_message(
await event.get_input_chat(), s, reply_to=m.id, parse_mode=None
)
last_msgs[chat_id].appendleft(out)
elif s is not None:
await event.reply(s)
raise events.StopPropagation

View File

@ -1,9 +1,6 @@
# 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/.
"""
.snips-ave snips and send them with .snip name
"""
import asyncio
from telethon import events, utils
from telethon.tl import types
@ -19,7 +16,7 @@ TYPE_DOCUMENT = 2
snips = storage.snips or {}
@borg.on(events.NewMessage(pattern=r'(?:\.snip\s+|!)(\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)
@ -39,7 +36,7 @@ async def on_snip(event):
reply_to=event.message.reply_to_msg_id)
@borg.on(events.NewMessage(pattern=r'\.snips\s+(\S+)', outgoing=True))
@borg.on(events.NewMessage(pattern=r'\.snips (\S+)', outgoing=True))
async def on_snip_save(event):
loop.create_task(event.delete())
name = event.pattern_match.group(1)

View File

@ -1,365 +0,0 @@
# 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/.
"""
Translates stuff into English
"""
import aiohttp
import asyncio
import io
import math
import mimetypes
import re
import time
from telethon import helpers, types
mimetypes.add_type('audio/mpeg', '.borg+tts')
LANGUAGES = {
'af': 'Afrikaans',
'sq': 'Albanian',
'am': 'Amharic',
'ar': 'Arabic',
'hy': 'Armenian',
'az': 'Azerbaijani',
'eu': 'Basque',
'be': 'Belarusian',
'bn': 'Bengali',
'bs': 'Bosnian',
'bg': 'Bulgarian',
'ca': 'Catalan',
'ceb': 'Cebuano',
'ny': 'Chichewa',
'zh-CN': 'Chinese (Simplified)',
'zh-TW': 'Chinese (Traditional)',
'co': 'Corsican',
'hr': 'Croatian',
'cs': 'Czech',
'da': 'Danish',
'nl': 'Dutch',
'en': 'English',
'eo': 'Esperanto',
'et': 'Estonian',
'tl': 'Filipino',
'fi': 'Finnish',
'fr': 'French',
'fy': 'Frisian',
'gl': 'Galician',
'ka': 'Georgian',
'de': 'German',
'el': 'Greek',
'gu': 'Gujarati',
'ht': 'Haitian Creole',
'ha': 'Hausa',
'haw': 'Hawaiian',
'iw': 'Hebrew',
'hi': 'Hindi',
'hmn': 'Hmong',
'hu': 'Hungarian',
'is': 'Icelandic',
'ig': 'Igbo',
'id': 'Indonesian',
'ga': 'Irish',
'it': 'Italian',
'ja': 'Japanese',
'jw': 'Javanese',
'kn': 'Kannada',
'kk': 'Kazakh',
'km': 'Khmer',
'rw': 'Kinyarwanda',
'ko': 'Korean',
'ku': 'Kurdish (Kurmanji)',
'ky': 'Kyrgyz',
'lo': 'Lao',
'la': 'Latin',
'lv': 'Latvian',
'lt': 'Lithuanian',
'lb': 'Luxembourgish',
'mk': 'Macedonian',
'mg': 'Malagasy',
'ms': 'Malay',
'ml': 'Malayalam',
'mt': 'Maltese',
'mi': 'Maori',
'mr': 'Marathi',
'mn': 'Mongolian',
'my': 'Myanmar (Burmese)',
'ne': 'Nepali',
'no': 'Norwegian',
'or': 'Odia (Oriya)',
'ps': 'Pashto',
'fa': 'Persian',
'pl': 'Polish',
'pt': 'Portuguese',
'pa': 'Punjabi',
'ro': 'Romanian',
'ru': 'Russian',
'sm': 'Samoan',
'gd': 'Scots Gaelic',
'sr': 'Serbian',
'st': 'Sesotho',
'sn': 'Shona',
'sd': 'Sindhi',
'si': 'Sinhala',
'sk': 'Slovak',
'sl': 'Slovenian',
'so': 'Somali',
'es': 'Spanish',
'su': 'Sundanese',
'sw': 'Swahili',
'sv': 'Swedish',
'tg': 'Tajik',
'ta': 'Tamil',
'tt': 'Tatar',
'te': 'Telugu',
'th': 'Thai',
'tr': 'Turkish',
'tk': 'Turkmen',
'uk': 'Ukrainian',
'ur': 'Urdu',
'ug': 'Uyghur',
'uz': 'Uzbek',
'vi': 'Vietnamese',
'cy': 'Welsh',
'xh': 'Xhosa',
'yi': 'Yiddish',
'yo': 'Yoruba',
'zu': 'Zulu'
}
def split_text(text, n=40):
words = text.split()
while len(words) > n:
comma = None
semicolon = None
for i in reversed(range(n)):
if words[i].endswith('.'):
yield ' '.join(words[:i + 1])
words = words[i + 1:]
break
elif not semicolon and words[i].endswith(';'):
semicolon = i + 1
elif not comma and words[i].endswith(','):
comma = i + 1
else:
cut = semicolon or comma or n
yield ' '.join(words[:cut])
words = words[cut:]
if words:
yield ' '.join(words)
class Translator:
_TKK_RE = re.compile(r"tkk:'(\d+)\.(\d+)'", re.DOTALL)
_BASE_URL = 'https://translate.google.com'
_TRANSLATE_URL = 'https://translate.google.com/translate_a/single'
_TRANSLATE_TTS_URL = 'https://translate.google.com/translate_tts'
_HEADERS = {
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0'
}
def __init__(self, target='en', source='auto'):
self._target = target
self._source = source
self._session = aiohttp.ClientSession(headers=self._HEADERS)
self._tkk = None
self._tkk_lock = asyncio.Lock()
async def _fetch_tkk(self):
async with self._session.get(self._BASE_URL) as resp:
html = await resp.text()
return tuple(map(int, self._TKK_RE.search(html).groups()))
def _need_refresh_tkk(self):
return (self._tkk is None) or (self._tkk[0] != int(time.time() / 3600))
def _calc_token(self, text):
"""
Original code by ultrafunkamsterdam/googletranslate:
https://github.com/ultrafunkamsterdam/googletranslate/blob/bd3f4d0a1386ffa634c8ebbebb3603279f3ece99/googletranslate/__init__.py#L263
If this ever breaks, the way it was found was in one of the top-100
longest lines of `translate_m.js` used by translate.google.com, it
uses a single-line with all these "magic" values and one can look
around there and use a debugger to figure out how it works. It's
a very straight-forward port.
"""
def xor_rot(a, b):
size_b = len(b)
c = 0
while c < size_b - 2:
d = b[c + 2]
d = ord(d[0]) - 87 if 'a' <= d else int(d)
d = (a % 0x100000000) >> d if '+' == b[c + 1] else a << d
a = a + d & 4294967295 if '+' == b[c] else a ^ d
c += 3
return a
a = []
text = helpers.add_surrogate(text)
for i in text:
val = ord(i)
if val < 0x10000:
a += [val]
else:
a += [
math.floor((val - 0x10000) / 0x400 + 0xD800),
math.floor((val - 0x10000) % 0x400 + 0xDC00),
]
d = self._tkk
b = d[0]
e = []
g = 0
size = len(text)
while g < size:
l = a[g]
if l < 128:
e.append(l)
else:
if l < 2048:
e.append(l >> 6 | 192)
else:
if (
(l & 64512) == 55296
and g + 1 < size
and a[g + 1] & 64512 == 56320
):
g += 1
l = 65536 + ((l & 1023) << 10) + (a[g] & 1023)
e.append(l >> 18 | 240)
e.append(l >> 12 & 63 | 128)
else:
e.append(l >> 12 | 224)
e.append(l >> 6 & 63 | 128)
e.append(l & 63 | 128)
g += 1
a = b
for i, value in enumerate(e):
a += value
a = xor_rot(a, '+-a^+6')
a = xor_rot(a, '+-3^+b+-f')
a ^= d[1]
if a < 0:
a = (a & 2147483647) + 2147483648
a %= 1000000
return '{}.{}'.format(a, a ^ b)
async def translate(self, text, target=None, source=None):
if self._need_refresh_tkk():
async with self._tkk_lock:
self._tkk = await self._fetch_tkk()
params = [
('client', 'webapp'),
('sl', source or self._source),
('tl', target or self._target),
('hl', 'en'),
*[('dt', x) for x in ['at', 'bd', 'ex', 'ld', 'md', 'qca', 'rw', 'rm', 'sos', 'ss', 't']],
('ie', 'UTF-8'),
('oe', 'UTF-8'),
('otf', 1),
('ssel', 0),
('tsel', 0),
('tk', self._calc_token(text)),
('q', text),
]
async with self._session.get(self._TRANSLATE_URL, params=params) as resp:
data = await resp.json()
return ''.join(part[0] for part in data[0] if part[0] is not None)
async def tts(self, text, target=None):
if self._need_refresh_tkk():
async with self._tkk_lock:
self._tkk = await self._fetch_tkk()
parts = list(split_text(text))
result = b''
for i, part in enumerate(parts):
params = [
('ie', 'UTF-8'),
('q', part),
('tl', target or self._target),
('total', len(parts)),
('idx', i),
('textlen', len(helpers.add_surrogate(part))),
('tk', self._calc_token(part)),
('client', 'webapp'),
('prev', 'input'),
]
async with self._session.get(self._TRANSLATE_TTS_URL, params=params) as resp:
if resp.status == 404:
raise ValueError('unknown target language')
else:
result += await resp.read()
return result
async def close(self):
await self._session.close()
translator = Translator()
@borg.on(borg.cmd(r"tl"))
async def _(event):
if event.is_reply:
text = (await event.get_reply_message()).raw_text
elif not borg.me.bot:
text = ''
started = False
async for m in borg.iter_messages(event.chat_id):
if started and m.sender_id == borg.uid:
break
if m.sender_id != borg.uid:
started = True
if not started or not m.raw_text:
continue
if ' ' in m.raw_text:
text = m.raw_text + '\n' + text
else:
text = m.raw_text + ' ' + text
else:
return
translated = await translator.translate(text.strip())
action = event.edit if not borg.me.bot else event.respond
await action('translation: ' + translated, parse_mode=None)
@borg.on(borg.cmd(r"tts"))
async def _(event):
if not borg.me.bot:
await event.delete()
ts = event.raw_text.split(maxsplit=1)
text = None if len(ts) < 2 else ts[1]
if not text and event.is_reply:
text = (await event.get_reply_message()).raw_text
if not text:
return
file = io.BytesIO(await translator.tts(text))
file.name = 'a.borg+tts'
await borg.send_file(
event.chat_id,
file,
reply_to=event.reply_to_msg_id if not borg.me.bot else None,
attributes=[types.DocumentAttributeAudio(
duration=0,
voice=True
)]
)
async def unload():
await translator.close()

View File

@ -1,22 +1,15 @@
# 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/.
"""
Show information about the chat or replied user
"""
import datetime
import html
import time
from telethon import events
from telethon import utils
from telethon.tl import types, functions
from telethon.tl import types
def get_who_string(who, rank=None):
def get_who_string(who):
who_string = html.escape(utils.get_display_name(who))
if rank is not None:
who_string += f' <i>"{html.escape(rank)}"</i>'
if isinstance(who, (types.User, types.Channel)) and who.username:
who_string += f" <i>(@{who.username})</i>"
who_string += f", <a href='tg://user?id={who.id}'>#{who.id}</a>"
@ -25,7 +18,6 @@ def get_who_string(who, rank=None):
@borg.on(events.NewMessage(pattern=r"\.who", outgoing=True))
async def _(event):
rank = None
if not event.message.is_reply:
who = await event.get_chat()
else:
@ -36,30 +28,14 @@ async def _(event):
msg.forward.from_id or msg.forward.channel_id)
else:
who = await msg.get_sender()
ic = await event.get_input_chat()
if isinstance(ic, types.InputPeerChannel):
rank = getattr((await borg(functions.channels.GetParticipantRequest(
ic,
who
))).participant, 'rank', None)
await event.edit(get_who_string(who, rank), parse_mode='html')
await event.edit(get_who_string(who), parse_mode='html')
@borg.on(events.NewMessage(pattern=r"\.members", outgoing=True))
async def _(event):
last = 0
index = 0
members = []
it = borg.iter_participants(event.chat_id)
async for member in it:
index += 1
now = time.time()
if now - last > 0.5:
last = now
await event.edit(f'counting member stats ({index / it.total:.2%})…')
async for member in borg.iter_participants(event.chat_id):
messages = await borg.get_messages(
event.chat_id,
from_user=member,
@ -74,25 +50,3 @@ async def _(event):
)
await event.edit("\n".join(members), parse_mode='html')
@borg.on(events.NewMessage(pattern=r"\.active_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=1
)
date = (messages[0].date if messages
else datetime.datetime.fromtimestamp(0, tz=datetime.timezone.utc))
members.append((
date,
f"{date:%Y-%m-%d} - {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')

View File

@ -5,13 +5,14 @@
import asyncio
import traceback
from uniborg import util
DELETE_TIMEOUT = 2
@borg.on(borg.admin_cmd(r"(?:re)?load", r"(?P<shortname>\w+)"))
@borg.on(util.admin_cmd(r"^\.(?:re)?load (?P<shortname>\w+)$"))
async def load_reload(event):
if not borg.me.bot:
await event.delete()
await event.delete()
shortname = event.pattern_match["shortname"]
try:
@ -21,9 +22,8 @@ async def load_reload(event):
msg = await event.respond(
f"Successfully (re)loaded plugin {shortname}")
if not borg.me.bot:
await asyncio.sleep(DELETE_TIMEOUT)
await borg.delete_messages(msg.to_id, msg)
await asyncio.sleep(DELETE_TIMEOUT)
await borg.delete_messages(msg.to_id, msg)
except Exception as e:
tb = traceback.format_exc()
@ -31,10 +31,9 @@ async def load_reload(event):
await event.respond(f"Failed to (re)load plugin {shortname}: {e}")
@borg.on(borg.admin_cmd(r"(?:unload|disable|remove)", r"(?P<shortname>\w+)"))
@borg.on(util.admin_cmd(r"^\.(?:unload|disable|remove) (?P<shortname>\w+)$"))
async def remove(event):
if not borg.me.bot:
await event.delete()
await event.delete()
shortname = event.pattern_match["shortname"]
if shortname == "_core":
@ -45,19 +44,5 @@ async def remove(event):
else:
msg = await event.respond(f"Plugin {shortname} is not loaded")
if not borg.me.bot:
await asyncio.sleep(DELETE_TIMEOUT)
await borg.delete_messages(msg.to_id, msg)
@borg.on(borg.admin_cmd(r"plugins"))
async def list_plugins(event):
result = f'{len(borg._plugins)} plugins loaded:'
for name, mod in sorted(borg._plugins.items(), key=lambda t: t[0]):
desc = (mod.__doc__ or '__no description__').replace('\n', ' ').strip()
result += f'\n**{name}**: {desc}'
if not borg.me.bot:
await event.edit(result)
else:
await event.respond(result)
await asyncio.sleep(DELETE_TIMEOUT)
await borg.delete_messages(msg.to_id, msg)

View File

@ -17,7 +17,7 @@ from . import hacks
class Uniborg(TelegramClient):
def __init__(
self, session, *, plugin_path="plugins", storage=None, admins=[],
self, session, *, plugin_path="plugins", storage=None,
bot_token=None, **kwargs):
# TODO: handle non-string session
#
@ -28,7 +28,6 @@ class Uniborg(TelegramClient):
self._logger = logging.getLogger(session)
self._plugins = {}
self._plugin_path = plugin_path
self.admins = admins
kwargs = {
"api_id": 6, "api_hash": "eb06d4abfb49dc3eeb1aeb98ae0f581e",
@ -109,32 +108,3 @@ class Uniborg(TelegramClient):
lambda _: self.remove_event_handler(cb, event_matcher))
return fut
def cmd(self, command, pattern=None, admin_only=False):
if self.me.bot:
command = fr'{command}(?:@{self.me.username})?'
if pattern is not None:
pattern = fr'{command}\s+{pattern}'
else:
pattern = command
if not self.me.bot:
pattern=fr'^\.{pattern}'
else:
pattern=fr'^\/{pattern}'
pattern=fr'(?i){pattern}$'
if self.me.bot and admin_only:
allowed_users = self.admins
else:
allowed_users = None
return telethon.events.NewMessage(
outgoing=not self.me.bot,
from_users=allowed_users,
pattern=pattern
)
def admin_cmd(self, command, pattern=None):
return self.cmd(command, pattern, admin_only=True)

View File

@ -10,6 +10,10 @@ from telethon import events
from telethon.tl.functions.messages import GetPeerDialogsRequest
def admin_cmd(pattern):
return events.NewMessage(outgoing=True, pattern=re.compile(pattern))
async def is_read(borg, entity, message, is_out=None):
"""
Returns True if the given message (or id) has been read