Compare commits

...

32 Commits

Author SHA1 Message Date
Dan Elkouby cc98721120 Improve command support
- Support commands with the bot's username
- Make matching case-insensitive by default
- Merge cmd and admin_cmd for better maintainability
2020-08-02 15:32:44 +00:00
Dan Elkouby b97d1649a4 Port tl plugin to bots 2020-08-02 15:08:46 +00:00
Dan Elkouby dd49e13fbf Merge "plugins" plugin into _core 2020-08-02 14:47:19 +00:00
Dan Elkouby 3813ed60e8 Add stdbot, to showcase plugins that work from a bot account 2020-08-02 14:42:25 +00:00
Dan Elkouby 475c3aebfd Rework admin_cmd to work for actual bots 2020-08-02 14:35:18 +00:00
Dan Elkouby c4de997eb7 sed: Add provisions for GirlBot 2020-08-02 14:10:04 +00:00
Dan Elkouby 22c9942c2d sed: refactor to look more like current reegee code 2020-08-02 14:01:02 +00:00
Lonami 269a2ab1ae Add list of supported languages 2020-06-11 11:50:46 +02:00
Lonami defca3cb25 Split long TTS messages 2020-05-22 09:44:32 +02:00
Lonami c7d81b26df Nicer .tts behaviour with replies 2020-05-09 18:15:41 +02:00
Lonami c9fbc78cda Enhance translator with TTS API access 2020-05-09 18:13:39 +02:00
Lonami fc945c6912 Fix markdown parse failing if message was None 2020-04-29 13:58:26 +02:00
Lonami 3e782066da Fix translation plugin when emoji are involved 2020-04-29 13:53:22 +02:00
Lonami 9de2e8083b Add translation plugin 2020-04-26 13:03:48 +02:00
Lonami e44f0befe4 Stuff 2020-04-21 17:29:39 +00:00
Dan Elkouby d18b85a709 stdplugins/plugins: fix 2020-04-06 14:50:05 +00:00
Dan Elkouby bff5fd02dd sed: only allow outgoing commands in groups
This plugin was originally intended for using in PM only.
Leaving it wide open in groups might create unwanted spam in case
another bot is present but not blacklisted. It may also violate "no
bots" policies in some groups.
By only allowing self to use the plugin in groups, we can still use its
features when no bot is present, while avoiding the above problems.
2020-02-28 10:01:15 +00:00
Dan Elkouby 1988c5821d sed: blacklist ani (oubot) 2020-02-27 21:08:21 +00:00
Lonami Exo 9dedb14d7c Briefly document all plugins 2020-02-24 16:08:15 +01:00
Lonami c770af06d2 Add plugins plugin to show plugged-in plugins 2020-02-24 15:53:40 +01:00
Dan Elkouby cb28f86038 Use the walrus instead of a for loop 2020-02-13 17:09:45 +00:00
Dan Elkouby 5c0b30cb15 Update chain plugin to cache results 2020-01-11 23:54:59 +00:00
Dan Elkouby 59b2fc7412 Fix active_members when there are no messages 2019-12-31 22:58:06 +00:00
Dan Elkouby 0ca6f4eebd Add fix for audio files sending as voice notes 2019-11-04 18:56:35 +00:00
Lonami a6ea92b859 Revert accidental .active_members removal 2019-10-21 12:21:28 +02:00
Lonami 508bf638bd Make .members fancy 2019-10-21 12:18:32 +02:00
Dan Elkouby 0953826ee3 Add member list by last active 2019-10-09 20:08:52 +00:00
Lonami b6a67cf5f2 Fix .who in private and reoder admin title 2019-10-08 17:41:00 +02:00
Lonami 95d95701ea Show admin rank title if available 2019-10-07 16:53:10 +02:00
expectocode 01ae7b1cc6 Make \0 work as expected 2019-09-13 20:19:17 +02:00
Lonami 29a47c73bf Fix replies in dab brain fp 2019-09-12 17:59:11 +02:00
Lonami e1648fe3b1 Add facepalm stickers 2019-09-12 17:45:15 +02:00
19 changed files with 668 additions and 94 deletions

1
botplugins/sed.py Symbolic link
View File

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

1
botplugins/tl.py Symbolic link
View File

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

18
stdbot.py Normal file
View File

@ -0,0 +1,18 @@
# 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,7 +1,9 @@
# 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,23 +1,48 @@
# 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):
await event.edit("Counting...")
count = -1
message = event.message
while message:
reply = await message.get_reply_message()
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)")
if reply is None:
await borg(SaveDraftRequest(
await event.get_input_chat(),
"",
reply_to_msg_id=message.id
reply_to_msg_id=message
))
message = reply
count += 1
if count:
storage.cache = global_cache
await event.edit(f"Chain length: {count}")

View File

@ -1,6 +1,9 @@
# 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,7 +1,9 @@
# 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,7 +1,9 @@
# 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,6 +1,9 @@
# 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
@ -93,6 +96,9 @@ MATCHERS = [
def parse(message, old_entities=None):
if not message:
return message
entities = []
old_entities = sorted(old_entities or [], key=lambda e: e.offset)

26
stdplugins/music_fix.py Normal file
View File

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

View File

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

View File

@ -1,15 +1,20 @@
from collections import defaultdict, deque
import re
"""
Become @regexbot when the bot is missing
"""
import regex as re
from collections import defaultdict, deque
import regex
from telethon import events, utils
from telethon.tl import types, functions
from uniborg import util
HEADER = "「sed」\n"
SED_PATTERN = r'^s/((?:\\/|[^/])+)/((?:\\/|[^/])*)(/.*)?'
GROUP0_RE = re.compile(r'(?<!\\)((?:\\\\)*)\\0')
HEADER = '' if borg.me.bot else '「sed」\n'
KNOWN_RE_BOTS = re.compile(
r'(regex|moku|BananaButler_|rgx|l4mR)bot',
r'(regex|moku|ou|BananaButler_|rgx|l4mR|ProgrammingAndGirls)bot',
flags=re.IGNORECASE
)
@ -19,11 +24,20 @@ 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)
def cleanup_pattern(match):
from_ = 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:
@ -35,34 +49,49 @@ def doit(chat_id, match, original):
# Build Python regex flags
count = 1
flags = 0
for f in fl:
for f in fl.lower():
if f == 'i':
flags |= regex.IGNORECASE
flags |= re.IGNORECASE
elif f == 'm':
flags |= re.MULTILINE
elif f == 's':
flags |= re.DOTALL
elif f == 'g':
count = 0
elif f == 'x':
flags |= re.VERBOSE
else:
return None, f"Unknown flag: {f}"
await message.reply(f'{HEADER}Unknown flag: {f}')
return
def actually_doit(original):
try:
s = original.message
def substitute(m):
if s := m.raw_text:
if s.startswith(HEADER):
s = s[len(HEADER):]
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
else:
return None
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
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}')
async def group_has_sedbot(group):
@ -75,38 +104,34 @@ async def group_has_sedbot(group):
return any(KNOWN_RE_BOTS.match(x.username or '') for x in full.users)
@borg.on(events.NewMessage)
async def on_message(event):
last_msgs[event.chat_id].appendleft(event.message)
@borg.on(events.MessageEdited)
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):
async def sed(event):
if event.fwd_from:
return
if not event.is_private and\
await group_has_sedbot(await event.get_input_chat()):
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
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)
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)
@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
borg.on(events.NewMessage(pattern=SED_PATTERN))(sed)
borg.on(events.MessageEdited(pattern=SED_PATTERN))(sed)

View File

@ -1,6 +1,9 @@
# 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
@ -16,7 +19,7 @@ TYPE_DOCUMENT = 2
snips = storage.snips or {}
@borg.on(events.NewMessage(pattern=r'(?:\.snip +|!)(\w+)$', outgoing=True))
@borg.on(events.NewMessage(pattern=r'(?:\.snip\s+|!)(\S+)$', outgoing=True))
async def on_snip(event):
loop.create_task(event.delete())
name = event.pattern_match.group(1)
@ -36,7 +39,7 @@ async def on_snip(event):
reply_to=event.message.reply_to_msg_id)
@borg.on(events.NewMessage(pattern=r'\.snips (\S+)', outgoing=True))
@borg.on(events.NewMessage(pattern=r'\.snips\s+(\S+)', outgoing=True))
async def on_snip_save(event):
loop.create_task(event.delete())
name = event.pattern_match.group(1)

365
stdplugins/tl.py Normal file
View File

@ -0,0 +1,365 @@
# 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,15 +1,22 @@
# 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
from telethon.tl import types, functions
def get_who_string(who):
def get_who_string(who, rank=None):
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>"
@ -18,6 +25,7 @@ def get_who_string(who):
@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:
@ -28,14 +36,30 @@ 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), parse_mode='html')
await event.edit(get_who_string(who, rank), parse_mode='html')
@borg.on(events.NewMessage(pattern=r"\.members", outgoing=True))
async def _(event):
last = 0
index = 0
members = []
async for member in borg.iter_participants(event.chat_id):
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%})…')
messages = await borg.get_messages(
event.chat_id,
from_user=member,
@ -50,3 +74,25 @@ 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,14 +5,13 @@
import asyncio
import traceback
from uniborg import util
DELETE_TIMEOUT = 2
@borg.on(util.admin_cmd(r"^\.(?:re)?load (?P<shortname>\w+)$"))
@borg.on(borg.admin_cmd(r"(?:re)?load", r"(?P<shortname>\w+)"))
async def load_reload(event):
await event.delete()
if not borg.me.bot:
await event.delete()
shortname = event.pattern_match["shortname"]
try:
@ -22,8 +21,9 @@ async def load_reload(event):
msg = await event.respond(
f"Successfully (re)loaded plugin {shortname}")
await asyncio.sleep(DELETE_TIMEOUT)
await borg.delete_messages(msg.to_id, msg)
if not borg.me.bot:
await asyncio.sleep(DELETE_TIMEOUT)
await borg.delete_messages(msg.to_id, msg)
except Exception as e:
tb = traceback.format_exc()
@ -31,9 +31,10 @@ async def load_reload(event):
await event.respond(f"Failed to (re)load plugin {shortname}: {e}")
@borg.on(util.admin_cmd(r"^\.(?:unload|disable|remove) (?P<shortname>\w+)$"))
@borg.on(borg.admin_cmd(r"(?:unload|disable|remove)", r"(?P<shortname>\w+)"))
async def remove(event):
await event.delete()
if not borg.me.bot:
await event.delete()
shortname = event.pattern_match["shortname"]
if shortname == "_core":
@ -44,5 +45,19 @@ async def remove(event):
else:
msg = await event.respond(f"Plugin {shortname} is not loaded")
await asyncio.sleep(DELETE_TIMEOUT)
await borg.delete_messages(msg.to_id, msg)
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)

View File

@ -17,7 +17,7 @@ from . import hacks
class Uniborg(TelegramClient):
def __init__(
self, session, *, plugin_path="plugins", storage=None,
self, session, *, plugin_path="plugins", storage=None, admins=[],
bot_token=None, **kwargs):
# TODO: handle non-string session
#
@ -28,6 +28,7 @@ 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",
@ -108,3 +109,32 @@ 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,10 +10,6 @@ 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