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 # 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 # 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/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""
Mentions .all people in the group
"""
from telethon import events from telethon import events

View File

@ -1,48 +1,23 @@
# This Source Code Form is subject to the terms of the Mozilla Public # 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 # 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/. # 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 import events
from telethon.tl.functions.messages import SaveDraftRequest 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)) @borg.on(events.NewMessage(pattern=r"\.chain", outgoing=True))
async def _(event): async def _(event):
cache = global_cache[event.chat_id] await event.edit("Counting...")
count = -1
message = event.reply_to_msg_id message = event.message
count = 0 while message:
while message is not None: reply = await message.get_reply_message()
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: if reply is None:
await borg(SaveDraftRequest( await borg(SaveDraftRequest(
await event.get_input_chat(), await event.get_input_chat(),
"", "",
reply_to_msg_id=message reply_to_msg_id=message.id
)) ))
message = reply message = reply
count += 1 count += 1
if count:
storage.cache = global_cache
await event.edit(f"Chain length: {count}") 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 # 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 # 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/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""
Reply with .fixreply to adjust your last message's reply
"""
import asyncio import asyncio
from telethon import events from telethon import events

View File

@ -1,9 +1,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public # 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 # 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/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""
.fpost a message with single-letter forwards
"""
import string import string
from telethon import events from telethon import events

View File

@ -1,9 +1,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public # 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 # 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/. # 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 import events
from telethon.utils import add_surrogate from telethon.utils import add_surrogate
from telethon.tl.types import MessageEntityPre 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 # 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 # 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/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""
Enhances your markdown capabilities
"""
import re import re
from functools import partial from functools import partial
@ -96,9 +93,6 @@ MATCHERS = [
def parse(message, old_entities=None): def parse(message, old_entities=None):
if not message:
return message
entities = [] entities = []
old_entities = sorted(old_entities or [], key=lambda e: e.offset) 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 # 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 # 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/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""
Perform ninja deletions or edits
"""
import asyncio import asyncio
from telethon import events from telethon import events
@ -39,8 +37,8 @@ async def await_read(chat, message):
await fut await fut
@borg.on(events.NewMessage(outgoing=True, pattern=r"^\.(del)(?:ete)?$") @borg.on(util.admin_cmd(r"^\.(del)(?:ete)?$"))
@borg.on(events.NewMessage(outgoing=True, pattern=r"^\.(edit)(?:\s+(.*))?$") @borg.on(util.admin_cmd(r"^\.(edit)(?:\s+(.*))?$"))
async def delete(event): async def delete(event):
await event.delete() await event.delete()
command = event.pattern_match.group(1) 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 # 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 # 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/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""
Send a random .brain or .dab sticker
"""
import random import random
from telethon import events, types, functions, utils from telethon import events, types, functions, utils
@ -23,7 +21,7 @@ def choser(cmd, pack, blacklist={}):
if x.id not in 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') choser('brain', 'supermind')
@ -42,8 +40,4 @@ choser('dab', 'DabOnHaters', {
1653974154589767863, 1653974154589767863,
1653974154589767852, 1653974154589767852,
1653974154589768677 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 from collections import defaultdict, deque
import re
import regex
from telethon import events, utils from telethon import events, utils
from telethon.tl import types, functions from telethon.tl import types, functions
from uniborg import util from uniborg import util
SED_PATTERN = r'^s/((?:\\/|[^/])+)/((?:\\/|[^/])*)(/.*)?' HEADER = "「sed」\n"
GROUP0_RE = re.compile(r'(?<!\\)((?:\\\\)*)\\0')
HEADER = '' if borg.me.bot else '「sed」\n'
KNOWN_RE_BOTS = re.compile( KNOWN_RE_BOTS = re.compile(
r'(regex|moku|ou|BananaButler_|rgx|l4mR|ProgrammingAndGirls)bot', r'(regex|moku|BananaButler_|rgx|l4mR)bot',
flags=re.IGNORECASE flags=re.IGNORECASE
) )
@ -24,20 +19,11 @@ KNOWN_RE_BOTS = re.compile(
last_msgs = defaultdict(lambda: deque(maxlen=10)) last_msgs = defaultdict(lambda: deque(maxlen=10))
def cleanup_pattern(match): @util.sync_timeout(1)
from_ = match.group(1) def doit(chat_id, match, original):
fr = match.group(1)
to = match.group(2) to = match.group(2)
to = to.replace('\\/', '/') 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: try:
fl = match.group(3) fl = match.group(3)
if fl is None: if fl is None:
@ -49,49 +35,34 @@ async def doit(message, match):
# Build Python regex flags # Build Python regex flags
count = 1 count = 1
flags = 0 flags = 0
for f in fl.lower(): for f in fl:
if f == 'i': if f == 'i':
flags |= re.IGNORECASE flags |= regex.IGNORECASE
elif f == 'm':
flags |= re.MULTILINE
elif f == 's':
flags |= re.DOTALL
elif f == 'g': elif f == 'g':
count = 0 count = 0
elif f == 'x':
flags |= re.VERBOSE
else: else:
await message.reply(f'{HEADER}Unknown flag: {f}') return None, f"Unknown flag: {f}"
return
def substitute(m): def actually_doit(original):
if s := m.raw_text: try:
s = original.message
if s.startswith(HEADER): if s.startswith(HEADER):
s = s[len(HEADER):] s = s[len(HEADER):]
else: s, i = regex.subn(fr, to, s, count=count, flags=flags)
return None 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 original is not None:
if i > 0: return actually_doit(original)
return s # Try matching the last few messages
for original in last_msgs[chat_id]:
try: m, s = actually_doit(original)
msg = None if s is not None:
substitution = None return m, s
if message.is_reply: return None, None
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): 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) 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) @borg.on(events.NewMessage)
async def catch_all(event): async def on_message(event):
last_msgs[event.chat_id].append(event.message) last_msgs[event.chat_id].appendleft(event.message)
@borg.on(events.MessageEdited) @borg.on(events.MessageEdited)
async def catch_edit(event): async def on_edit(event):
for i, message in enumerate(last_msgs[event.chat_id]): for m in last_msgs[event.chat_id]:
if message.id == event.id: if m.id == event.id:
last_msgs[event.chat_id][i] = event.message 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) chat_id = utils.get_peer_id(await event.get_input_chat())
borg.on(events.MessageEdited(pattern=SED_PATTERN))(sed)
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 # 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 # 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/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""
.snips-ave snips and send them with .snip name
"""
import asyncio import asyncio
from telethon import events, utils from telethon import events, utils
from telethon.tl import types from telethon.tl import types
@ -19,7 +16,7 @@ TYPE_DOCUMENT = 2
snips = storage.snips or {} 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): async def on_snip(event):
loop.create_task(event.delete()) loop.create_task(event.delete())
name = event.pattern_match.group(1) name = event.pattern_match.group(1)
@ -39,7 +36,7 @@ async def on_snip(event):
reply_to=event.message.reply_to_msg_id) 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): async def on_snip_save(event):
loop.create_task(event.delete()) loop.create_task(event.delete())
name = event.pattern_match.group(1) 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 # 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 # 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/. # 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 html
import time
from telethon import events from telethon import events
from telethon import utils 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)) 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: if isinstance(who, (types.User, types.Channel)) and who.username:
who_string += f" <i>(@{who.username})</i>" who_string += f" <i>(@{who.username})</i>"
who_string += f", <a href='tg://user?id={who.id}'>#{who.id}</a>" 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)) @borg.on(events.NewMessage(pattern=r"\.who", outgoing=True))
async def _(event): async def _(event):
rank = None
if not event.message.is_reply: if not event.message.is_reply:
who = await event.get_chat() who = await event.get_chat()
else: else:
@ -36,30 +28,14 @@ async def _(event):
msg.forward.from_id or msg.forward.channel_id) msg.forward.from_id or msg.forward.channel_id)
else: else:
who = await msg.get_sender() 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)) @borg.on(events.NewMessage(pattern=r"\.members", outgoing=True))
async def _(event): async def _(event):
last = 0
index = 0
members = [] 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( messages = await borg.get_messages(
event.chat_id, event.chat_id,
from_user=member, from_user=member,
@ -74,25 +50,3 @@ async def _(event):
) )
await event.edit("\n".join(members), parse_mode='html') 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 asyncio
import traceback import traceback
from uniborg import util
DELETE_TIMEOUT = 2 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): async def load_reload(event):
if not borg.me.bot: await event.delete()
await event.delete()
shortname = event.pattern_match["shortname"] shortname = event.pattern_match["shortname"]
try: try:
@ -21,9 +22,8 @@ async def load_reload(event):
msg = await event.respond( msg = await event.respond(
f"Successfully (re)loaded plugin {shortname}") f"Successfully (re)loaded plugin {shortname}")
if not borg.me.bot: await asyncio.sleep(DELETE_TIMEOUT)
await asyncio.sleep(DELETE_TIMEOUT) await borg.delete_messages(msg.to_id, msg)
await borg.delete_messages(msg.to_id, msg)
except Exception as e: except Exception as e:
tb = traceback.format_exc() tb = traceback.format_exc()
@ -31,10 +31,9 @@ async def load_reload(event):
await event.respond(f"Failed to (re)load plugin {shortname}: {e}") 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): async def remove(event):
if not borg.me.bot: await event.delete()
await event.delete()
shortname = event.pattern_match["shortname"] shortname = event.pattern_match["shortname"]
if shortname == "_core": if shortname == "_core":
@ -45,19 +44,5 @@ async def remove(event):
else: else:
msg = await event.respond(f"Plugin {shortname} is not loaded") msg = await event.respond(f"Plugin {shortname} is not loaded")
if not borg.me.bot: await asyncio.sleep(DELETE_TIMEOUT)
await asyncio.sleep(DELETE_TIMEOUT) await borg.delete_messages(msg.to_id, msg)
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): class Uniborg(TelegramClient):
def __init__( def __init__(
self, session, *, plugin_path="plugins", storage=None, admins=[], self, session, *, plugin_path="plugins", storage=None,
bot_token=None, **kwargs): bot_token=None, **kwargs):
# TODO: handle non-string session # TODO: handle non-string session
# #
@ -28,7 +28,6 @@ class Uniborg(TelegramClient):
self._logger = logging.getLogger(session) self._logger = logging.getLogger(session)
self._plugins = {} self._plugins = {}
self._plugin_path = plugin_path self._plugin_path = plugin_path
self.admins = admins
kwargs = { kwargs = {
"api_id": 6, "api_hash": "eb06d4abfb49dc3eeb1aeb98ae0f581e", "api_id": 6, "api_hash": "eb06d4abfb49dc3eeb1aeb98ae0f581e",
@ -109,32 +108,3 @@ class Uniborg(TelegramClient):
lambda _: self.remove_event_handler(cb, event_matcher)) lambda _: self.remove_event_handler(cb, event_matcher))
return fut 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 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): async def is_read(borg, entity, message, is_out=None):
""" """
Returns True if the given message (or id) has been read Returns True if the given message (or id) has been read