Compare commits

..

100 Commits
master ... kate

Author SHA1 Message Date
udf 692cc6b9ac
remove plugins that i dont want 2019-09-09 09:41:27 +02:00
udf 080f18302c
Merge branch 'master' into kate 2019-09-09 09:39:50 +02:00
udf 27d7b5ed82
Merge branch 'master' of https://git.togrand.xyz/uniborg/uniborg 2019-09-09 09:35:39 +02:00
udf 21f80a953d
fuck you lonami you fucking retard
"but dan made the commit"
fuck you, you dont know shit, lonami came up with the idea to use ++
2019-08-14 23:54:48 +02:00
udf 3178eb5fba
Increase length limits when sending html 2019-08-03 17:46:28 +02:00
udf 59245a4e6d
Move constants to function parameters 2019-08-03 17:43:37 +02:00
udf 6dc069afd9
Send html file when message is too long 2019-08-03 17:41:48 +02:00
udf 5dd06471db
lonami you piece of shit 2019-07-29 22:38:13 +02:00
udf bd271b7e29
remove explicit setting in toggle preview 2019-07-07 00:01:35 +02:00
udf e810a977c5
Merge branch 'master' into kate 2019-05-19 23:11:15 +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
udf 30c4f29b9c
add delete plugin 2019-05-07 15:16:47 +02:00
udf 8290974235
make gif square with dark background 2019-05-03 23:15:30 +02:00
udf 49ac424183
add thing 2019-05-02 23:17:41 +02:00
udf fc78927fa6
Merge branch 'kate' of github.com:udf/uniborg into kate 2019-04-29 20:09:09 +02:00
udf 9f61d75ec9
fix 2019-04-29 20:04:21 +02:00
udf 5fa301f563
remove me up inside 2019-04-29 20:01:59 +02:00
udf 0948bd1d5b fix 2019-04-27 13:51:54 +00:00
udf 3b71397a37
remove unused function 2019-04-27 12:22:42 +02:00
udf ccd47186b1
make it work for different chats 2019-04-27 12:22:23 +02:00
udf d2c6607429
echo user who changed the group title 2019-04-27 12:15:40 +02:00
udf 0b64a64568
remove waiting for deletion 2019-04-27 12:05:46 +02:00
udf b98bc8eb02
remove useless caching 2019-04-27 12:01:40 +02:00
udf a3981e0994
fix logic by using a task to schedule title reverting 2019-04-02 22:50:29 +02:00
udf fd20c8804b
rename lock 2019-04-02 22:21:26 +02:00
udf 787660b16f
await instead of creating task for deletion 2019-01-17 11:33:20 +02:00
udf 1756ebad66
Merge branch 'master' into kate 2019-01-17 11:28:27 +02:00
udf 06ff8e59de
press H for heuristics 2019-01-14 18:31:32 +02:00
udf 4927d143e4
add moar shitpost 2019-01-12 14:10:01 +02:00
udf 71288d61eb
Merge branch 'master' into kate 2018-12-26 23:35:41 +02:00
udf dd086369ca
load api key from module 2018-12-26 23:28:41 +02:00
udf d9e3240c5c
Merge branch 'master' into kate 2018-12-23 14:22:12 +02:00
udf 54c399f7d5
Merge branch 'kate' of git.togrand.xyz:kate/uniborg into kate 2018-12-18 00:26:33 +02:00
kate 4271767df3 mewge bwanch 'master' of lonami/uniborg into kate =w=
UwU
2018-12-17 23:06:29 +01:00
Lonami 2a5e9aac94 Simplify code 2018-12-17 22:21:16 +01:00
udf 5275a27020
allow "..e" to edit to empty
also clean-up code but no one reads these
2018-12-07 16:25:58 +02:00
udf 926ea5751c
fix chat not being defined 2018-12-07 15:54:11 +02:00
udf 165b100804
yank to current chat instead of saved messages 2018-12-07 14:40:31 +02:00
udf 7a90830621
fix undefined variable 2018-11-28 16:19:14 +02:00
udf f751ed707d
refactor get_target_message into get_recent_self_message 2018-11-28 16:16:01 +02:00
udf 100f417744
Only get self message from history if not a reply 2018-11-28 16:07:49 +02:00
udf 7426cea6d2
Remove delay before making draft and only fire on \n.e 2018-11-28 15:09:29 +02:00
udf e43d3b29bf
move file_to_photo out of kbass suite 2018-11-26 19:59:43 +02:00
udf 1a5a7a3210
fix image extension error and copy target caption 2018-11-26 19:58:35 +02:00
udf ed18b3ccba
Add check for large files and comments 2018-11-25 02:45:20 +02:00
udf b0b2b65ca4
add short descriptions to kbass plugins 2018-11-24 23:22:44 +02:00
udf 4f05e252be
add tdesktop keyboard assistance plugins 2018-11-24 23:14:04 +02:00
udf 599f794525
move get_target_message to utils 2018-11-24 17:19:23 +02:00
udf 4d531be35b
catch exceptions when the title isnt modified 2018-11-22 16:02:48 +02:00
udf dbbd02edc8
Merge branch 'ninja_fix' into kate 2018-11-22 00:40:00 +02:00
udf 7f789c23c9
Merge branch 'superblock' into kate 2018-11-21 22:55:49 +02:00
udf cf7c642b18
Merge branch 'master' into kate 2018-11-21 22:55:02 +02:00
udf 8c0f04c0cb
Add license header 2018-11-21 22:08:27 +02:00
udf 3e5b6fddb3
Add superblock plugin 2018-11-21 22:08:27 +02:00
udf 2a740440d5
Add superblock plugin 2018-11-21 21:05:33 +02:00
udf 28f43b5bc8
use shield() correctly 2018-11-14 16:34:29 +02:00
udf 3ea3808ac2
fix not waiting after rename 2018-11-14 16:22:43 +02:00
udf 7c90f97a97
fix typo 2018-11-14 00:44:11 +02:00
udf 23b6f78cbd
revert title if message is deleted before natural revert 2018-11-14 00:37:10 +02:00
udf 785b215c90
make constants uppercase 2018-11-14 00:33:35 +02:00
udf 24f5b88d3a
Merge branch 'master' of https://github.com/udf/uniborg 2018-11-13 22:41:33 +02:00
udf c43114264f
fix links 2018-11-09 21:08:59 +02:00
udf dfa87dc849
Merge branch 'master' of https://git.togrand.xyz/uniborg/uniborg 2018-11-04 14:55:59 +02:00
udf 7041489498
Merge branch 'master' of https://github.com/udf/uniborg 2018-11-03 14:19:09 +02:00
udf e246a8d378
remove debug code; move length limit to constants 2018-11-03 14:18:59 +02:00
udf a146d4f3b4
Useless imports are the best imports 2018-11-03 05:08:46 +02:00
udf 225e377130
truncate really long byte sequences 2018-11-03 04:58:51 +02:00
udf 192e1d890a
hide empty objects 2018-11-03 04:53:32 +02:00
udf 774c0bebdb
add message .info plugin 2018-11-03 04:39:21 +02:00
udf f4a7e49cc1
remove old stuff 2018-11-03 03:10:49 +02:00
udf ddeb468a79
remove stock tags from markdown
now it just adds new tags instead of replicating the default behaviour of the clients
2018-11-02 18:25:04 +02:00
udf 5da937756b
fix fix 2018-11-01 23:24:17 +02:00
udf f9b3c48a11
move sp_kick_invite to disabled
previously it was unreleased
2018-11-01 23:17:30 +02:00
udf 9981c27415
Merge branch 'master' of https://github.com/udf/uniborg 2018-11-01 23:10:14 +02:00
udf b28f2c5b35
attempt to fix title casing 2018-11-01 23:04:15 +02:00
udf 2ba0debfea
clean up imports 2018-11-01 23:02:34 +02:00
udf 61d03d4870 Merge https://git.togrand.xyz/uniborg/uniborg 2018-10-08 19:21:24 +00:00
udf 829828f835 move sp_h to disabled dir 2018-10-08 18:58:45 +00:00
udf 608aea3e5b shitpost harder 2018-09-13 23:00:00 +02:00
udf c19c70291c Merge branch 'master' of https://github.com/udf/uniborg 2018-09-13 22:55:45 +02:00
udf 3ef1a7fd11 reply with braille art on deletion 2018-09-13 22:54:57 +02:00
udf a923b74c6f
Merge pull request #1 from Qwerty-Space/patch-1
made smol
2018-08-27 00:07:48 +02:00
Qwerty-Space c3ed26a1c7
made smol 2018-08-26 22:57:45 +01:00
udf 73dd73eb24 h 2018-08-26 23:28:26 +02:00
udf 699ac98b05 Merge https://github.com/uniborg/uniborg 2018-07-19 16:56:32 +00:00
udf c4f8779e34 Merge branch 'master' of https://github.com/uniborg/uniborg 2018-06-30 20:23:04 +00:00
udf 39017da8dc update to latest uniborg master 2018-06-22 16:26:32 +00:00
udf 5c93052045 Merge branch 'master' of https://github.com/uniborg/uniborg 2018-06-17 20:10:17 +00:00
udf 2d016d534d remove shit 2018-06-16 13:37:56 +00:00
udf dfd0e87735 Merge branch 'master' of https://github.com/uniborg/uniborg 2018-06-16 13:37:45 +00:00
EyeZiS b2bb1c6027 -S 2018-06-14 20:08:04 +02:00
udf ade16c6655 Merge branch 'master' of https://github.com/uniborg/uniborg 2018-06-12 19:54:43 +02:00
udf a1b255b8a1 Merge branch 'fix-reparse' of https://github.com/uniborg/uniborg 2018-06-10 19:57:35 +00:00
udf b6b262f93d Merge branch 'master' of https://github.com/uniborg/uniborg 2018-06-10 19:55:14 +00:00
udf 64f971ee64 make thing better, idk 2018-06-10 19:54:00 +00:00
udf a07a352d1c use None as default parameter instead of empty list 2018-06-10 21:10:48 +02:00
udf a2a4b506cb Skip existing entities
It's a very hacky PoC, but it fixes things like urls being incorrectly parsed
2018-06-10 20:40:15 +02:00
eyezis d04d082c45 add shitpost plugins 2018-06-03 14:16:44 +02:00
eyezis 95fd4eec97 remove unused plugins 2018-06-03 14:16:00 +02:00
37 changed files with 674 additions and 880 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
venv
__pycache__
api_key.py
*.session
*.session-journal
data/

View File

@ -8,7 +8,7 @@ Pluggable [``asyncio``](https://docs.python.org/3/library/asyncio.html)
Simply clone the repository and run the main file:
```sh
git clone https://github.com/uniborg/uniborg.git
git clone https://github.com/udf/uniborg.git
cd uniborg
python stdborg.py
```
@ -16,11 +16,11 @@ python stdborg.py
## design
The modular design of the project enhances your Telegram experience
through [plugins](https://github.com/uniborg/uniborg/tree/master/stdplugins)
through [plugins](https://github.com/udf/uniborg/tree/master/stdplugins)
which you can enable or disable on demand.
Each plugin gets the `borg`, `logger` and `storage` magical
[variables](https://github.com/uniborg/uniborg/blob/4805f2f6de7d734c341bb978318f44323ad525f1/uniborg/uniborg.py#L66-L68)
[variables](https://github.com/udf/uniborg/blob/4805f2f6de7d734c341bb978318f44323ad525f1/uniborg/uniborg.py#L66-L68)
to ease their use. Thus creating a plugin as easy as adding
a new file under the plugin directory to do the job:
@ -36,12 +36,12 @@ async def handler(event):
## internals
The core features offered by the custom `TelegramClient` live under the
[`uniborg/`](https://github.com/uniborg/uniborg/tree/master/uniborg)
[`uniborg/`](https://github.com/udf/uniborg/tree/master/uniborg)
directory, with some utilities, enhancements and the core plugin.
## learning
Check out the already-mentioned
[plugins](https://github.com/uniborg/uniborg/tree/master/stdplugins)
[plugins](https://github.com/udf/uniborg/tree/master/stdplugins)
directory to learn how to write your own, and consider reading
[Telethon's documentation](http://telethon.readthedocs.io/).

2
api_key.example.py Normal file
View File

@ -0,0 +1,2 @@
id = 12345
hash = "11223344556677889900AABBCCDDEEFF"

View File

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

View File

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

10
disabledplugins/sp_bar.py Normal file
View File

@ -0,0 +1,10 @@
import asyncio
from telethon import events
from telethon.tl import types
import re
@borg.on(events.NewMessage(pattern=re.compile(r"^bar$"), chats=1040270887))
async def on_bar(event):
if event.from_id == 151462131:
return
await event.reply("foo")

37
disabledplugins/sp_h.py Normal file
View File

@ -0,0 +1,37 @@
import asyncio
from telethon import events
from telethon.tl import types
EPIC_MEME = (
"""
"""
)
@borg.on(events.NewMessage(pattern=r"^h$"))
async def on_h(event):
await borg.reply(file="CAADAQADpgIAAna32gVJ62JcFcDnqwI")
async def del_filter(del_event):
if del_event.chat_id and del_event.chat_id != event.chat_id:
return False
return del_event.deleted_id == message.id
fut = borg.await_event(events.MessageDeleted, del_filter)
try:
await asyncio.wait_for(fut, timeout=5)
await event.reply(EPIC_MEME)
except asyncio.TimeoutError:
pass

View File

@ -0,0 +1,26 @@
# kicks people who join a group by invite
import asyncio
from telethon import events
from telethon.tl.functions.channels import EditBannedRequest
from telethon.tl.types import ChannelBannedRights
import time
channel_id = 1252035294
@borg.on(events.ChatAction(chats=channel_id))
async def on_join(event):
if event.user_joined:
await borg(
EditBannedRequest(
channel=channel_id,
user_id=await event.get_user(),
banned_rights=ChannelBannedRights(
time.time() + 60,
True, True, True, True, True, True, True, True
)
)
)

View File

@ -5,9 +5,16 @@
import logging
from uniborg import Uniborg
import api_key
logging.basicConfig(level=logging.INFO)
borg = Uniborg("stdborg", plugin_path="stdplugins", connection_retries=None)
borg = Uniborg(
"stdborg",
plugin_path="stdplugins",
connection_retries=None,
api_id=api_key.id,
api_hash=api_key.hash
)
borg.run_until_disconnected()

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,20 +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/.
"""
Mentions .all people in the group
"""
from telethon import events
@borg.on(events.NewMessage(pattern=r"\.all", outgoing=True))
async def _(event):
if event.fwd_from:
return
await event.delete()
mentions = "@all"
chat = await event.get_input_chat()
async for x in borg.iter_participants(chat, 100):
mentions += f"[\u2063](tg://user?id={x.id})"
await borg.send_message(
chat, mentions, reply_to=event.message.reply_to_msg_id)

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

@ -0,0 +1,42 @@
"""
Reply to a file with .f to send it as a photo
"""
from io import BytesIO
from uniborg import util
from telethon import types
from telethon.errors import PhotoInvalidDimensionsError
from telethon.tl.functions.messages import SendMediaRequest
@borg.on(util.admin_cmd(r"^\.f$"))
async def on_file_to_photo(event):
await event.delete()
target = await event.get_reply_message()
try:
image = target.media.document
except AttributeError:
return
if not image.mime_type.startswith('image/'):
return # This isn't an image
if image.mime_type == 'image/webp':
return # Telegram doesn't let you directly send stickers as photos
if image.size > 10 * 1024 * 1024:
return # We'd get PhotoSaveFileInvalidError otherwise
file = await borg.download_media(target, file=BytesIO())
file.seek(0)
img = await borg.upload_file(file)
img.name = 'image.png'
try:
await borg(SendMediaRequest(
peer=await event.get_input_chat(),
media=types.InputMediaUploadedPhoto(img),
message=target.message,
entities=target.entities,
reply_to_msg_id=target.id
))
except PhotoInvalidDimensionsError:
return

View File

@ -1,30 +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/.
"""
Reply with .fixreply to adjust your last message's reply
"""
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)
])

View File

@ -1,29 +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/.
"""
.fpost a message with single-letter forwards
"""
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])

View File

@ -0,0 +1,89 @@
from io import BytesIO
from uniborg import util
from PIL import Image
from telethon import types, utils, events
from telethon.tl.functions.messages import SaveGifRequest, UploadMediaRequest
sticker_to_gif = storage.sticker_to_gif or {}
access_hashes = storage.access_hashes or {}
gif_to_sticker = {str(gif): int(sticker) for sticker, gif in sticker_to_gif.items()}
async def convert_sticker_to_gif(sticker):
gif_id = sticker_to_gif.get(str(sticker.id), None)
if gif_id:
access_hash = access_hashes[str(gif_id)]
return types.InputDocument(gif_id, access_hash, b'')
file = BytesIO()
await borg.download_media(sticker, file=file)
file.seek(0)
# remove alpha
im = Image.open(file)
alpha = im.convert('RGBA').getchannel('A')
size = max(im.width, im.height)
new_im = Image.new('RGBA', (size, size), (40, 40, 40, 255))
xy = (round((size - im.width) / 2), round((size - im.height) / 2))
new_im.paste(im, box=xy, mask=alpha)
file = BytesIO()
new_im.save(file, format='gif')
file.seek(0)
# upload file
file = await borg.upload_file(file, part_size_kb=512)
file = types.InputMediaUploadedDocument(file, 'video/mp4', [])
media = await borg(UploadMediaRequest('me', file))
media = utils.get_input_document(media)
# save (that's right, this is relational json)
sticker_to_gif[str(sticker.id)] = media.id
gif_to_sticker[str(media.id)] = sticker.id
access_hashes[str(sticker.id)] = sticker.access_hash
access_hashes[str(media.id)] = media.access_hash
storage.sticker_to_gif = sticker_to_gif
storage.access_hashes = access_hashes
return media
@borg.on(util.admin_cmd(r'^\.ss$'))
async def on_save(event):
await event.delete()
target = await event.get_reply_message()
media = target.gif or target.sticker
if not media:
return
if target.sticker:
media = await convert_sticker_to_gif(media)
await borg(
SaveGifRequest(id=media, unsave=False)
)
@borg.on(events.NewMessage(outgoing=True))
async def on_sticker(event):
if not event.sticker:
return
media = await convert_sticker_to_gif(event.sticker)
await borg(
SaveGifRequest(id=media, unsave=False)
)
@borg.on(events.NewMessage(outgoing=True))
async def on_gif(event):
if not event.gif:
return
sticker_id = gif_to_sticker.get(str(event.gif.id), None)
if not sticker_id:
return
access_hash = access_hashes[str(sticker_id)]
sticker = types.InputDocument(sticker_id, access_hash, b'')
await event.delete()
await borg.send_message(
await event.get_input_chat(),
file=sticker,
reply_to=event.message.reply_to_msg_id
)

View File

@ -1,18 +1,14 @@
# 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
from telethon.tl.types import MessageEntityPre, DocumentAttributeFilename
from telethon.tl.tlobject import TLObject
from telethon.errors import MessageTooLongError
import datetime
STR_LEN_MAX = 256
BYTE_LEN_MAX = 64
def parse_pre(text):
text = text.strip()
@ -22,7 +18,7 @@ def parse_pre(text):
)
def yaml_format(obj, indent=0):
def yaml_format(obj, indent=0, max_str_len=256, max_byte_len=64):
"""
Pretty formats the given object as a YAML string which is returned.
(based on TLObject.pretty_format)
@ -57,8 +53,8 @@ def yaml_format(obj, indent=0):
indent -= 2
elif isinstance(obj, str):
# truncate long strings and display elipsis
result = repr(obj[:STR_LEN_MAX])
if len(obj) > STR_LEN_MAX:
result = repr(obj[:max_str_len])
if len(obj) > max_str_len:
result += ''
return result
elif isinstance(obj, bytes):
@ -66,7 +62,7 @@ def yaml_format(obj, indent=0):
if all(0x20 <= c < 0x7f for c in obj):
return repr(obj)
else:
return ('<…>' if len(obj) > BYTE_LEN_MAX else
return ('<…>' if len(obj) > max_byte_len else
' '.join(f'{b:02X}' for b in obj))
elif isinstance(obj, datetime.datetime):
# ISO-8601 without timezone offset (telethon dates are always UTC)
@ -92,4 +88,21 @@ async def _(event):
return
msg = await event.message.get_reply_message()
yaml_text = yaml_format(msg)
await event.edit(yaml_text, parse_mode=parse_pre)
try:
await event.edit(yaml_text, parse_mode=parse_pre)
except MessageTooLongError:
await event.delete()
yaml_text = yaml_format(
msg,
max_str_len=9999,
max_byte_len=9999
)
await borg.send_file(
await event.get_input_chat(),
f'<pre>{yaml_text}</pre>'.encode('utf-8'),
reply_to=msg,
attributes=[
DocumentAttributeFilename('info.html')
],
allow_cache=False
)

59
stdplugins/kbass_core.py Normal file
View File

@ -0,0 +1,59 @@
"""
Contains code used by other kbass_* plugins
"""
from uniborg import util
async def get_target_message(borg, event):
"""
If the event is a reply, returns the reply message if it's from us
If event is not a reply, then it tries to return the most recent message
from us
"""
target = await event.get_reply_message()
if event.is_reply and target.from_id == borg.uid:
return target
if not target:
return await util.get_recent_self_message(borg, event)
def self_reply_cmd(borg, pattern):
def wrapper(function):
@borg.on(util.admin_cmd(pattern))
async def wrapped(event, *args, **kwargs):
await event.delete()
target = await get_target_message(borg, event)
if not target:
return
return await function(event, target, *args, **kwargs)
return wrapped
return wrapper
def self_reply_selector(borg, cmd):
def wrapper(function):
@borg.on(util.admin_cmd(cmd + r"( [+-]?\d+)?$"))
async def wrapped(event, *args, **kwargs):
await event.delete()
reply = await event.get_reply_message()
if not reply:
return
num_offset = int(event.pattern_match.group(1) or 0)
reverse = num_offset > 0
targets = [reply] if reverse else []
targets.extend(await borg.get_messages(
await event.get_input_chat(),
limit=abs(num_offset),
offset_id=reply.id,
reverse=reverse
))
if not reverse:
# reverse the list because we want things to always be in
# history order
targets.reverse()
targets.append(reply)
return await function(event, targets, num_offset, *args, **kwargs)
return wrapped
return wrapper

View File

@ -0,0 +1,15 @@
"""
Reply to a message with .d <n> to delete <n> messages from that point
(negative values of <n> go backwards in history)
"""
import asyncio
from stdplugins.kbass_core import self_reply_selector
@self_reply_selector(borg, r'\.d')
async def on_save(event, targets, num_offset):
await borg.delete_messages(
await event.get_input_chat(),
targets
)

42
stdplugins/kbass_edit.py Normal file
View File

@ -0,0 +1,42 @@
"""
Reply to a message with .e to make a draft of it (with '\n.e' appended)
Reply to your own message with <text>.e to edit the message to <text>
if <text> is ".", the message is edited to empty/deleted
"""
import asyncio
from telethon.errors import MessageEmptyError
from telethon.tl.functions.messages import SaveDraftRequest
from telethon.tl.functions.messages import EditMessageRequest
from stdplugins.kbass_core import self_reply_cmd
@self_reply_cmd(borg, r"^\.e$")
async def on_edit_start(event, target):
await borg(SaveDraftRequest(
peer=await event.get_input_chat(),
message=(target.message or '.') + '\n.e',
entities=target.entities,
no_webpage=not target.media,
reply_to_msg_id=target.id
))
@self_reply_cmd(borg, r'(?ms)^(.+\n|\.)\.e$')
async def on_edit_end(event, target):
text = event.pattern_match.group(1)
if text == '.':
text = ''
chat = await event.get_input_chat()
try:
await borg(EditMessageRequest(
peer=chat,
id=target.id,
no_webpage=not target.media,
message=text,
entities=event.message.entities
))
except MessageEmptyError:
# Can't make text message empty, so delete it
await borg.delete_messages(chat, target)

15
stdplugins/kbass_save.py Normal file
View File

@ -0,0 +1,15 @@
"""
Reply to a message with .s <n> to forward <n> messages from that point to your
saved messages (negative values of <n> go backwards in history)
"""
import asyncio
from stdplugins.kbass_core import self_reply_selector
@self_reply_selector(borg, r'\.s')
async def on_save(event, targets, num_offset):
await borg.forward_messages('me', targets)
msg = await event.respond(f'Saved {abs(num_offset) + 1} messages!')
await asyncio.sleep(3)
await borg.delete_messages(msg.to_id, msg)

View File

@ -0,0 +1,22 @@
"""
Reply to a message with .p to toggle the webpage preview of a message
"""
from telethon.errors import MessageNotModifiedError
from telethon.tl.functions.messages import EditMessageRequest
from stdplugins.kbass_core import self_reply_cmd
@self_reply_cmd(borg, r"^\.p$")
async def on_edit_preview(event, target):
try:
await borg(EditMessageRequest(
peer=await event.get_input_chat(),
id=target.id,
no_webpage=bool(target.media),
message=target.message,
entities=target.entities
))
except MessageNotModifiedError:
# There was no preview to modify
pass

28
stdplugins/kbass_yank.py Normal file
View File

@ -0,0 +1,28 @@
"""
Like save but makes a draft in your chat of all the messages concatenated with
two newlines
"""
import html
from telethon.tl.functions.messages import SaveDraftRequest
from telethon.extensions import html as thtml
from stdplugins.kbass_core import self_reply_selector
def get_message_html(message):
if message.action:
return html.escape(str(message.action))
return thtml.unparse(message.message, message.entities)
@self_reply_selector(borg, r'\.y')
async def on_yank(event, targets, num_offset):
message = '\n\n'.join(get_message_html(target) for target in targets)
message, entities = thtml.parse(message)
await borg(SaveDraftRequest(
peer=await event.get_input_chat(),
message=message,
entities=entities,
no_webpage=True,
))

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
@ -11,10 +8,7 @@ from telethon import events
from telethon.tl.functions.messages import EditMessageRequest
from telethon.extensions.markdown import DEFAULT_URL_RE
from telethon.utils import add_surrogate, del_surrogate
from telethon.tl.types import (
MessageEntityBold, MessageEntityItalic, MessageEntityCode,
MessageEntityPre, MessageEntityTextUrl
)
from telethon.tl.types import MessageEntityTextUrl
def parse_url_match(m):
@ -26,14 +20,6 @@ def parse_url_match(m):
return m.group(1), entity
def get_tag_parser(tag, entity):
# TODO unescape escaped tags?
def tag_parser(m):
return m.group(1), entity(offset=m.start(), length=len(m.group(1)))
tag = re.escape(tag)
return re.compile(tag + r'(.+?)' + tag, re.DOTALL), tag_parser
def parse_aesthetics(m):
def aesthetify(string):
for c in string:
@ -75,19 +61,11 @@ def parse_snip(m):
return m.group(1), None
PARSED_ENTITIES = (
MessageEntityBold, MessageEntityItalic, MessageEntityCode,
MessageEntityPre, MessageEntityTextUrl
)
# A matcher is a tuple of (regex pattern, parse function)
# where the parse function takes the match and returns (text, entity)
MATCHERS = [
(DEFAULT_URL_RE, parse_url_match),
(get_tag_parser('**', MessageEntityBold)),
(get_tag_parser('__', MessageEntityItalic)),
(get_tag_parser('```', partial(MessageEntityPre, language=''))),
(get_tag_parser('`', MessageEntityCode)),
(re.compile(r'\+\+(.+?)\+\+'), parse_aesthetics),
(re.compile(r'!\+(.+?)\+!'), parse_aesthetics),
(re.compile(r'~~(.+?)~~'), parse_strikethrough),
(re.compile(r'@@(.+?)@@'), parse_enclosing_circle),
(re.compile(r'([^/\w]|^)(/?(r/\w+))'), parse_subreddit),
@ -96,9 +74,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
@ -16,10 +14,7 @@ from uniborg import util
async def get_target_message(event):
if event.is_reply and (await event.get_reply_message()).from_id == borg.uid:
return await event.get_reply_message()
async for message in borg.iter_messages(
await event.get_input_chat(), limit=20):
if message.out:
return message
return await util.get_recent_self_message(borg, event)
async def await_read(chat, message):
@ -39,8 +34,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,49 +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/.
"""
Send a random .brain or .dab sticker
"""
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), reply_to=event.reply_to_msg_id)
choser('brain', 'supermind')
choser('dab', 'DabOnHaters', {
1653974154589768377,
1653974154589768312,
1653974154589767857,
1653974154589768311,
1653974154589767816,
1653974154589767939,
1653974154589767944,
1653974154589767912,
1653974154589767911,
1653974154589767910,
1653974154589767909,
1653974154589767863,
1653974154589767852,
1653974154589768677
})
choser('fp', 'facepalmstickers', {
285892071401720411,
285892071401725809
})

View File

@ -1,137 +0,0 @@
"""
Become @regexbot when the bot is missing
"""
import regex as re
from collections import defaultdict, deque
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'
KNOWN_RE_BOTS = re.compile(
r'(regex|moku|ou|BananaButler_|rgx|l4mR|ProgrammingAndGirls)bot',
flags=re.IGNORECASE
)
# Heavily based on
# https://github.com/SijmenSchoon/regexbot/blob/master/regexbot.py
last_msgs = defaultdict(lambda: deque(maxlen=10))
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:
fl = ''
fl = fl[1:]
except IndexError:
fl = ''
# Build Python regex flags
count = 1
flags = 0
for f in fl.lower():
if f == 'i':
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:
await message.reply(f'{HEADER}Unknown flag: {f}')
return
def substitute(m):
if s := m.raw_text:
if s.startswith(HEADER):
s = s[len(HEADER):]
else:
return 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):
if isinstance(group, types.InputPeerChannel):
full = await borg(functions.channels.GetFullChannelRequest(group))
elif isinstance(group, types.InputPeerChat):
full = await borg(functions.messages.GetFullChatRequest(group.chat_id))
else:
return False
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)
@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,15 +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
loop = asyncio.get_event_loop()
TYPE_TEXT = 0
TYPE_PHOTO = 1
TYPE_DOCUMENT = 2
@ -19,9 +13,9 @@ 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())
await event.delete()
name = event.pattern_match.group(1)
if name not in snips:
return
@ -39,9 +33,9 @@ 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())
await event.delete()
name = event.pattern_match.group(1)
msg = await event.get_reply_message()
if not msg:
@ -67,20 +61,20 @@ async def on_snip_save(event):
@borg.on(events.NewMessage(pattern=r'\.snipl', outgoing=True))
async def on_snip_list(event):
loop.create_task(event.delete())
await event.delete()
await event.respond('available snips: ' + ', '.join(snips.keys()))
@borg.on(events.NewMessage(pattern=r'\.snipd (\S+)', outgoing=True))
async def on_snip_delete(event):
loop.create_task(event.delete())
await event.delete()
snips.pop(event.pattern_match.group(1), None)
storage.snips = snips
@borg.on(events.NewMessage(pattern=r'\.snipr (\S+)\s+(\S+)', outgoing=True))
async def on_snip_rename(event):
loop.create_task(event.delete())
await event.delete()
snip = snips.pop(event.pattern_match.group(1), None)
if snip:
snips[event.pattern_match.group(2)] = snip

View File

@ -0,0 +1,50 @@
# This code is licensed under the "you can't use this for anything - public or private, unless you know the two prime factors to the number below" license
# 815135719850789377883578725031646603830554299421513756734809391809510031541688408783808970346383033757707621582823614192302656899785568810141101893861881544237739149165045057025573497586408009729060573391256863917255487270653675297192518166563834141855446956044528844961195768446385460848417889889512327393559206631684434956840903011319062951892997125169227543717045951033991368795690797564380613059915726818757922302267383605155042461118479905071060213802110046981731900697376015815129214785100602795677501006290272973672300816405578389088779900256121674562690160929422533940362417144425257511815580506170724347283155944909375158732773438876520611705378823191171992189144811606615936656533338106330174876188601037745081068420279548019633259148082386504346352351578891810674889814082091097414272150489618713359903390399060803087577639221054129712178794984798651181559492842516823781833551514766471742367787744714913473089507256439629996620616758423114303082155000815398967237318664243052141156113838109051476103926243143220043202798142769625516248444575191662897109024478694074801968357243665353971378775679069823897126494990585348071113879526926445790538574160281772626205848696719803694382991806049568186743858598838394425423896463
import asyncio
from telethon import events
import re
blanks = (
' .'
'\u180e\u200b\u200c\u200d\u2060\u2061\u2062\u2063\u2064\u2066'
'\u2067\u2068\u2069\u206a\u206b\u206c\u206d\u206e\u206f\ufeff'
)
blanks_re = re.compile('|'.join(re.escape(c) for c in blanks))
person_id = 217924941
lonami_id = 10885151
@borg.on(events.NewMessage)
async def on_fwd_person(event):
if not event.fwd_from or event.fwd_from.from_id != person_id:
return
async def lonami_msg_filter(msg):
return msg.from_id == lonami_id
chat = await event.get_input_chat()
fut = borg.await_event(
events.NewMessage(chats=chat),
lonami_msg_filter
)
try:
msg_event = await asyncio.wait_for(fut, timeout=5)
await borg.delete_messages(chat, msg_event.message)
except asyncio.TimeoutError:
pass
@borg.on(events.NewMessage)
async def on_him_reply(event):
if event.from_id != lonami_id:
return
reply = await event.get_reply_message()
if not reply or not reply.fwd_from:
return
if reply and reply.fwd_from.from_id != person_id:
return
text = blanks_re.sub('', event.raw_text)
if text.startswith('>'):
await event.delete()

View File

@ -0,0 +1,95 @@
import asyncio
import re
import html
from dataclasses import dataclass
from telethon import events, utils
from telethon.tl.functions.channels import EditTitleRequest
from telethon.errors.rpcerrorlist import ChatNotModifiedError
MULTI_EDIT_TIMEOUT = 80
REVERT_TIMEOUT = 2 * 60 * 60
@dataclass
class Group:
id: int
title: str
rename_lock = asyncio.Lock()
revert_task: asyncio.Task = None
GROUPS = {group.id: group for group in (
# Group(1040270887, 'Programming & Tech'),
Group(1384391544, 'Programming & Tech for girls'),
Group(1286178907, 'test supergroup')
)}
def fix_title(s):
# Ideally this would be a state machine, but ¯\_(ツ)_/¯
def replace(m):
token = m.group(1)
if token.lower() == 'and':
token = '&'
return token[0].upper() + token[1:] + (' ' if m.group(2) else '')
return re.sub(r'(\S+)(\s+)?', replace, s)
async def edit_title(chat, title):
try:
await borg(EditTitleRequest(
channel=chat, title=title
))
except ChatNotModifiedError:
pass # Everything is ok
async def wait_and_revert(chat_id, title, timeout):
await asyncio.sleep(timeout)
await edit_title(chat_id, title)
@borg.on(events.NewMessage(
pattern=re.compile(r"(?i)programming (?:&|and) (.+)"),
chats=list(GROUPS.keys())))
async def on_name(event):
new_topic = fix_title(event.pattern_match.group(1))
new_title = f"Programming & {new_topic}"
if "Tech" not in new_title:
new_title += " & Tech"
group = GROUPS[utils.get_peer_id(event.chat_id, False)] # Thanks Lonami
logger.info(f'{event.from_id} in {group.id} '
f'requested a title change to {new_title}')
if len(new_title) > 255:
logger.info('Not changing group title because new title is too long')
return
if group.rename_lock.locked():
logger.info('Not changing group title because the rename lock is already held')
return
with (await group.rename_lock):
logger.info(f'Changing group title to {new_title}')
await event.respond(
f'<a href="tg://user?id={event.from_id}">{html.escape(event.sender.first_name)}</a>'
' changed the group title!',
parse_mode='html'
)
await edit_title(event.chat_id, new_title)
logger.info(f'Holding rename lock for {MULTI_EDIT_TIMEOUT} seconds')
await asyncio.sleep(MULTI_EDIT_TIMEOUT)
if group.revert_task and not group.revert_task.done():
logger.info('Cancelling previous revert task')
group.revert_task.cancel()
logger.info('Creating revert task')
group.revert_task = asyncio.create_task(wait_and_revert(
event.chat_id,
group.title,
REVERT_TIMEOUT
))

58
stdplugins/superblock.py Normal file
View File

@ -0,0 +1,58 @@
# 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
import telethon.tl.functions as tlf
from telethon.tl.types import InputPeerChannel, UpdateUserBlocked
from telethon.tl.functions.contacts import GetBlockedRequest
# How often to fetch the full list of blocked users
REFETCH_TIME = 60
blocked_user_ids = set()
@borg.on(events.NewMessage(incoming=True, func=lambda e: e.message.mentioned))
async def on_mentioned(event):
if not event.message.from_id: # Channel messages don't have a from_id
return
if event.from_id not in blocked_user_ids:
return
peer = await borg.get_input_entity(event.chat_id)
if isinstance(peer, InputPeerChannel):
o = tlf.channels.ReadMessageContentsRequest(peer, [event.message.id])
else:
o = tlf.messages.ReadMessageContentsRequest([event.message.id])
await borg(o)
@borg.on(events.Raw(types=UpdateUserBlocked))
async def on_blocked(event):
if event.blocked:
blocked_user_ids.add(event.user_id)
else:
blocked_user_ids.discard(event.user_id)
async def fetch_blocked_users():
global blocked_user_ids
while 1:
offset = 0
blocked_ids = set()
while 1:
blocked = await borg(GetBlockedRequest(offset=offset, limit=100))
offset += 100
for contact in blocked.blocked:
blocked_ids.add(contact.user_id)
if not blocked.blocked:
break
blocked_user_ids = blocked_ids
await asyncio.sleep(REFETCH_TIME)
asyncio.ensure_future(fetch_blocked_users())

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|enable|add) (?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
@ -27,6 +31,13 @@ async def is_read(borg, entity, message, is_out=None):
max_id = dialog.read_outbox_max_id if is_out else dialog.read_inbox_max_id
return message_id <= max_id
async def get_recent_self_message(borg, event):
async for message in borg.iter_messages(
await event.get_input_chat(), limit=20):
if message.out:
return message
def _handle_timeout(signum, frame):
raise TimeoutError("Execution took too long")