Compare commits

..

20 Commits

Author SHA1 Message Date
Tulir Asokan d033042ee1 Bump version to 0.14.2 2023-09-19 12:55:37 -04:00
Tulir Asokan 2270f4fe40 Update pillow 2023-09-19 10:59:15 -04:00
Tulir Asokan 6d208b37a5 Stringify sticker pack IDs and include sticker ID 2023-09-14 17:15:17 -04:00
Tulir Asokan 55ebaef6e3 Include sticker pack reference in events 2023-09-14 14:06:19 -04:00
Tulir Asokan 215f077cf0 Make forward backfill timeout configurable 2023-08-29 21:10:37 +03:00
Tulir Asokan 4e4f409f87 Update changelog 2023-08-27 00:52:38 +03:00
Tulir Asokan 4d145f4716 Update mautrix-python 2023-08-27 00:52:21 +03:00
Tulir Asokan b833a41a88 Update Telethon 2023-08-27 00:11:05 +03:00
Tulir Asokan 768d51c4ae Add fallback message for invoices 2023-08-19 12:09:22 +03:00
Tulir Asokan f7db298fda Ignore stories and story replies properly 2023-08-19 12:08:12 +03:00
Tulir Asokan 4f2118c7ee Fix sending media 2023-08-14 20:47:40 +03:00
Tulir Asokan 4f0770b92d Update Telethon 2023-08-13 17:40:39 +03:00
Sumner Evans 1fb8a7a0a5 stickers: passthrough webm and tgs files
I got the mime type of tgs files from here:
https://github.com/tulir/Telethon/blob/main/telethon/utils.py#L54

Signed-off-by: Sumner Evans <sumner@beeper.com>
2023-08-08 13:51:28 -06:00
Tulir Asokan f79ab283f3 Don't clear saved username based on min user object 2023-08-03 20:37:01 +03:00
Tulir Asokan 23ec691128 Handle "inactive" reactions when fetching allowed list 2023-07-21 12:33:28 +03:00
Tulir Asokan 59213ebeae Add warning log if GetAvailableReactions returns unexpected data 2023-07-06 13:26:01 +03:00
Tulir Asokan 36b2f6af2e Fix bridging reactions if server was rebooted less than 12 hours ago
Fixes #915
2023-07-06 13:24:13 +03:00
Tulir Asokan b2249f7756 Add log when message handling finishes 2023-07-06 13:24:10 +03:00
Tulir Asokan 212023d296 Don't send logout bridge state event if the user was already logged out 2023-07-03 19:21:24 +03:00
Tulir Asokan 4b03134620 Log when username changes 2023-07-03 19:20:12 +03:00
14 changed files with 147 additions and 35 deletions
+12
View File
@@ -1,3 +1,15 @@
# v0.14.2 (2023-09-19)
* **Security:** Updated Pillow to 10.0.1.
* Added support for double puppeting with arbitrary `as_token`s.
See [docs](https://docs.mau.fi/bridges/general/double-puppeting.html#appservice-method-new) for more info.
* Added support for sending webm and tgs files as stickers.
* Updated to Telegram API layer 161.
* Fixed cached usernames for Telegram users being cleared incorrectly, leading
to mentions not being bridged as usernames.
* Fixed reaction bridging failing if the server running the bridge was rebooted
less than 12 hours ago.
# v0.14.1 (2023-06-26)
### Added
+4 -2
View File
@@ -2,7 +2,7 @@ FROM dock.mau.dev/tulir/lottieconverter:alpine-3.18
RUN apk add --no-cache \
python3 py3-pip py3-setuptools py3-wheel \
py3-pillow \
#py3-pillow \
py3-aiohttp \
py3-magic \
py3-ruamel.yaml \
@@ -32,7 +32,9 @@ RUN apk add --no-cache \
bash \
curl \
jq \
yq
yq \
# Temporarily install pillow from edge repo to get up-to-date version
&& apk add --no-cache py3-pillow --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community
COPY requirements.txt /opt/mautrix-telegram/requirements.txt
COPY optional-requirements.txt /opt/mautrix-telegram/optional-requirements.txt
+1 -1
View File
@@ -1,2 +1,2 @@
__version__ = "0.14.1"
__version__ = "0.14.2"
__author__ = "Tulir Asokan <tulir@maunium.net>"
-2
View File
@@ -104,8 +104,6 @@ class TelegramBridge(Bridge):
self.log.info("Finished re-sending bridge info state events")
def prepare_stop(self) -> None:
for puppet in Puppet.by_custom_mxid.values():
puppet.stop()
self.add_shutdown_actions(user.stop() for user in User.by_tgid.values())
if self.bot:
self.add_shutdown_actions(self.bot.stop())
@@ -39,8 +39,6 @@ async def clear_db_cache(evt: CommandEvent) -> EventID:
await evt.reply("Cleared portal cache")
elif section == "puppet":
pu.Puppet.by_tgid = {}
for puppet in pu.Puppet.by_custom_mxid.values():
puppet.stop()
pu.Puppet.by_custom_mxid = {}
await asyncio.gather(
*[puppet.try_start() async for puppet in pu.Puppet.all_with_custom_mxid()]
@@ -69,8 +67,6 @@ async def reload_user(evt: CommandEvent) -> EventID:
if not user:
return await evt.reply("User not found")
puppet = await pu.Puppet.get_by_custom_mxid(mxid)
if puppet:
puppet.stop()
await user.stop()
del u.User.by_tgid[user.tgid]
del u.User.by_mxid[user.mxid]
+1
View File
@@ -194,6 +194,7 @@ class Config(BaseBridgeConfig):
copy("bridge.backfill.forward_limits.sync.normal_group")
copy("bridge.backfill.forward_limits.sync.supergroup")
copy("bridge.backfill.forward_limits.sync.channel")
copy("bridge.backfill.forward_timeout")
copy("bridge.backfill.incremental.messages_per_batch")
copy("bridge.backfill.incremental.post_batch_delay")
copy("bridge.backfill.incremental.max_batches.user")
+2
View File
@@ -420,6 +420,8 @@ bridge:
normal_group: 100
supergroup: 100
channel: 100
# Timeout for forward backfills in seconds. If you have a high limit, you'll have to increase this too.
forward_timeout: 900
# Settings for incremental backfill of history. These only apply when using MSC2716.
incremental:
+44 -14
View File
@@ -99,6 +99,7 @@ from telethon.tl.types import (
DocumentAttributeAudio,
DocumentAttributeFilename,
DocumentAttributeImageSize,
DocumentAttributeSticker,
DocumentAttributeVideo,
GeoPoint,
InputChannel,
@@ -110,6 +111,7 @@ from telethon.tl.types import (
InputPeerChat,
InputPeerPhotoFileLocation,
InputPeerUser,
InputStickerSetEmpty,
InputUser,
MessageActionChannelCreate,
MessageActionChatAddUser,
@@ -1841,6 +1843,7 @@ class Portal(DBPortal, BasePortal):
max_image_size = self.config["bridge.image_as_file_size"] * 1000**2
max_image_pixels = self.config["bridge.image_as_file_pixels"]
attributes = []
if self.config["bridge.parallel_file_transfer"] and content.url:
file_handle, file_size = await util.parallel_transfer_to_telegram(
client, self.main_intent, content.url, sender_id
@@ -1860,21 +1863,27 @@ class Portal(DBPortal, BasePortal):
file = await self.main_intent.download_media(content.url)
if content.msgtype == MessageType.STICKER:
if mime != "image/gif":
mime, file, w, h = util.convert_image(
file, source_mime=mime, target_type="webp"
)
else:
if mime == "image/gif":
# Remove sticker description
file_name = "sticker.gif"
else:
if mime not in ("video/webm", "application/x-tgsticker"):
mime, file, w, h = util.convert_image(
file, source_mime=mime, target_type="webp"
)
attributes.append(
DocumentAttributeSticker(
alt=content.body, stickerset=InputStickerSetEmpty()
)
)
file_handle = await client.upload_file(file)
file_size = len(file)
file_handle.name = file_name
force_document = file_size >= max_image_size
attributes.append(DocumentAttributeFilename(file_name=file_name))
attributes = [DocumentAttributeFilename(file_name=file_name)]
if content.msgtype == MessageType.VIDEO:
attributes.append(
DocumentAttributeVideo(
@@ -2047,7 +2056,7 @@ class Portal(DBPortal, BasePortal):
response: TypeMessage,
msgtype: MessageType | None = None,
) -> None:
self.log.trace("Handled Matrix message: %s", response)
self.log.trace("Raw event handling response for %s: %s", event_id, response)
event_hash, _ = self.dedup.check(response, (event_id, space), force_hash=edit_index != 0)
if edit_index < 0:
prev_edit = await DBMessage.get_one_by_tgid(TelegramID(response.id), space, -1)
@@ -2077,6 +2086,9 @@ class Portal(DBPortal, BasePortal):
seconds=response.ttl_period,
expires_at=int(response.date.timestamp()) + response.ttl_period,
)
self.log.debug(
f"Handled Matrix message {event_id} -> {response.id} (edit index {edit_index})"
)
@staticmethod
def _error_to_human_message(err: Exception) -> str | None:
@@ -2420,6 +2432,10 @@ class Portal(DBPortal, BasePortal):
await deleter.client(
SendReactionRequest(peer=self.peer, msg_id=msg.tgid, reaction=new_reactions)
)
self.log.debug(
f"Handled Matrix deletion of reaction {event_id} to {msg.tgid} "
f"(new reaction count: {len(new_reactions) if new_reactions else 0})"
)
async def _handle_matrix_deletion(self, deleter: u.User, event_id: EventID) -> None:
real_deleter = deleter if not await deleter.needs_relaybot(self) else self.bot
@@ -2440,6 +2456,7 @@ class Portal(DBPortal, BasePortal):
else:
await message.mark_redacted()
await real_deleter.client.delete_messages(self.peer, [message.tgid])
self.log.debug(f"Handled Matrix redaction of {event_id} / {message.tgid}")
async def handle_matrix_reaction(
self, user: u.User, target_event_id: EventID, emoji: str, reaction_event_id: EventID
@@ -2546,9 +2563,15 @@ class Portal(DBPortal, BasePortal):
SendReactionRequest(peer=self.peer, msg_id=msg.tgid, reaction=new_tg_reactions)
)
puppet = await user.get_puppet()
removed = 0
for db_reaction in reactions_to_remove:
removed += 1
await db_reaction.delete()
await puppet.intent_for(self).redact(db_reaction.mx_room, db_reaction.mxid)
self.log.debug(
f"Handled Matrix reaction {reaction_event_id} to {msg.tgid} "
f"(new reaction count: {len(new_tg_reactions)}, removed {removed} old reactions)"
)
await DBReaction(
mxid=reaction_event_id,
mx_room=self.mxid,
@@ -2862,13 +2885,15 @@ class Portal(DBPortal, BasePortal):
)
if limit == 0:
return "Limit is zero, not backfilling"
timeout = self.config["bridge.backfill.forward_timeout"]
with self.backfill_lock:
output = await asyncio.wait_for(
self.backfill(
source, client, forward=True, forward_limit=limit, last_tgid=last_tgid
),
timeout=15 * 60,
task = self.backfill(
source, client, forward=True, forward_limit=limit, last_tgid=last_tgid
)
if timeout > 0:
output = await asyncio.wait_for(task, timeout=timeout)
else:
output = await task
self.log.debug(f"Forward backfill complete, status: {output}")
return output
@@ -3225,9 +3250,13 @@ class Portal(DBPortal, BasePortal):
for item in counts:
if item.count == 2:
reactions += [
MessagePeerReaction(reaction=item.reaction, peer_id=PeerUser(self.tgid)),
MessagePeerReaction(
reaction=item.reaction, peer_id=PeerUser(self.tg_receiver)
reaction=item.reaction, peer_id=PeerUser(self.tgid), date=None
),
MessagePeerReaction(
reaction=item.reaction,
peer_id=PeerUser(self.tg_receiver),
date=None,
),
]
elif item.count == 1:
@@ -3235,6 +3264,7 @@ class Portal(DBPortal, BasePortal):
MessagePeerReaction(
reaction=item.reaction,
peer_id=PeerUser(self.tg_receiver if item.chosen_order else self.tgid),
date=None,
)
)
return reactions
@@ -34,6 +34,8 @@ from telethon.tl.types import (
DocumentAttributeVideo,
Game,
InputPhotoFileLocation,
InputStickerSetID,
InputStickerSetShortName,
Message,
MessageEntityPre,
MessageMediaContact,
@@ -42,11 +44,14 @@ from telethon.tl.types import (
MessageMediaGame,
MessageMediaGeo,
MessageMediaGeoLive,
MessageMediaInvoice,
MessageMediaPhoto,
MessageMediaPoll,
MessageMediaStory,
MessageMediaUnsupported,
MessageMediaVenue,
MessageMediaWebPage,
MessageReplyStoryHeader,
PeerChannel,
PeerUser,
Photo,
@@ -104,6 +109,7 @@ class DocAttrs(NamedTuple):
mime_type: str | None
is_sticker: bool
sticker_alt: str | None
sticker_pack_ref: dict | None
width: int
height: int
is_gif: bool
@@ -142,6 +148,8 @@ class TelegramMessageConverter:
MessageMediaUnsupported: self._convert_unsupported,
MessageMediaGame: self._convert_game,
MessageMediaContact: self._convert_contact,
MessageMediaStory: self._convert_story,
MessageMediaInvoice: self._convert_invoice,
}
self._allowed_media = tuple(self._media_converters.keys())
@@ -252,6 +260,8 @@ class TelegramMessageConverter:
) -> None:
if not evt.reply_to:
return
elif isinstance(evt.reply_to, MessageReplyStoryHeader):
return
space = (
evt.peer_id.channel_id
if isinstance(evt, Message) and isinstance(evt.peer_id, PeerChannel)
@@ -700,10 +710,33 @@ class TelegramMessageConverter:
)
return ConvertedMessage(content=content)
@staticmethod
async def _convert_story(
source: au.AbstractUser, evt: Message, client: MautrixTelegramClient, **_
) -> ConvertedMessage:
content = await formatter.telegram_to_matrix(
evt, source, client, override_text="Stories are not yet supported"
)
content.msgtype = MessageType.NOTICE
content["fi.mau.telegram.unsupported"] = True
return ConvertedMessage(content=content)
@staticmethod
async def _convert_invoice(
source: au.AbstractUser, evt: Message, client: MautrixTelegramClient, **_
) -> ConvertedMessage:
content = await formatter.telegram_to_matrix(
evt, source, client, override_text="Invoices are not yet supported"
)
content.msgtype = MessageType.NOTICE
content["fi.mau.telegram.unsupported"] = True
return ConvertedMessage(content=content)
def _parse_document_attributes(attributes: list[TypeDocumentAttribute]) -> DocAttrs:
name, mime_type, is_sticker, sticker_alt, width, height = None, None, False, None, 0, 0
is_gif, is_audio, is_voice, duration, waveform = False, False, False, 0, bytes()
sticker_pack_ref = None
for attr in attributes:
if isinstance(attr, DocumentAttributeFilename):
name = name or attr.file_name
@@ -711,6 +744,13 @@ def _parse_document_attributes(attributes: list[TypeDocumentAttribute]) -> DocAt
elif isinstance(attr, DocumentAttributeSticker):
is_sticker = True
sticker_alt = attr.alt
if isinstance(attr.stickerset, InputStickerSetID):
sticker_pack_ref = {
"id": str(attr.stickerset.id),
"access_hash": str(attr.stickerset.access_hash),
}
elif isinstance(attr.stickerset, InputStickerSetShortName):
sticker_pack_ref = {"short_name": attr.stickerset.short_name}
elif isinstance(attr, DocumentAttributeAnimated):
is_gif = True
elif isinstance(attr, DocumentAttributeVideo):
@@ -728,6 +768,7 @@ def _parse_document_attributes(attributes: list[TypeDocumentAttribute]) -> DocAt
mime_type,
is_sticker,
sticker_alt,
sticker_pack_ref,
width,
height,
is_gif,
@@ -760,6 +801,13 @@ def _parse_document_meta(
mime_type = file.mime_type or document.mime_type
info = ImageInfo(size=file.size, mimetype=mime_type)
if attrs.is_sticker:
info["fi.mau.telegram.sticker"] = {
"alt": attrs.sticker_alt,
"id": str(document.id),
"pack": attrs.sticker_pack_ref,
}
if attrs.mime_type and not file.was_converted:
file.mime_type = attrs.mime_type or file.mime_type
if file.width and file.height:
+6 -3
View File
@@ -269,11 +269,14 @@ class Puppet(DBPuppet, BasePuppet):
is_bot != self.is_bot or is_channel != self.is_channel or is_premium != self.is_premium
)
self.is_bot = is_bot
if is_bot is not None:
self.is_bot = is_bot
self.is_channel = is_channel
self.is_premium = is_premium
if is_premium is not None:
self.is_premium = is_premium
if self.username != info.username:
if self.username != info.username and (info.username or not info.min):
self.log.debug(f"Updating username {self.username} -> {info.username}")
self.username = info.username
changed = True
+6 -1
View File
@@ -22,6 +22,7 @@ from telethon.tl.patched import Message
from telethon.tl.types import (
InputMediaUploadedDocument,
InputMediaUploadedPhoto,
InputReplyToMessage,
TypeDocumentAttribute,
TypeInputMedia,
TypeInputPeer,
@@ -67,6 +68,10 @@ class MautrixTelegramClient(TelegramClient):
entity = await self.get_input_entity(entity)
reply_to = utils.get_message_id(reply_to)
request = SendMediaRequest(
entity, media, message=caption or "", entities=entities or [], reply_to_msg_id=reply_to
entity,
media,
message=caption or "",
entities=entities or [],
reply_to=InputReplyToMessage(reply_to_msg_id=reply_to) if reply_to else None,
)
return self._get_response_message(request, await self(request), entity)
+19 -4
View File
@@ -617,8 +617,11 @@ class User(DBUser, AbstractUser, BaseUser):
await self.stop()
await sess.delete()
# Drop LOGGED_OUT states if the user was already logged out previously
# and doesn't have a remote ID anymore
# TODO send a management room notice for non-manual logouts?
await self.push_bridge_state(state, error=error, message=message)
if self.tgid or state != BridgeStateEvent.LOGGED_OUT:
await self.push_bridge_state(state, error=error, message=message)
if delete:
await self.delete()
self.by_mxid.pop(self.mxid, None)
@@ -974,11 +977,18 @@ class User(DBUser, AbstractUser, BaseUser):
self.log.debug("Contact syncing complete")
return contacts
@property
def _available_reactions_up_to_date(self) -> bool:
return (
bool(self._available_emoji_reactions)
and self._available_emoji_reactions_fetched + 12 * 60 * 60 > time.monotonic()
)
async def get_available_reactions(self) -> set[str]:
if self._available_emoji_reactions_fetched + 12 * 60 * 60 > time.monotonic():
if self._available_reactions_up_to_date:
return self._available_emoji_reactions
async with self._available_emoji_reactions_lock:
if self._available_emoji_reactions_fetched + 12 * 60 * 60 > time.monotonic():
if self._available_reactions_up_to_date:
return self._available_emoji_reactions
self.log.debug("Fetching available emoji reactions")
available_reactions = await self.client(
@@ -988,13 +998,18 @@ class User(DBUser, AbstractUser, BaseUser):
self._available_emoji_reactions = {
react.reaction
for react in available_reactions.reactions
if self.is_premium or not react.premium
if not react.inactive and (self.is_premium or not react.premium)
}
self._available_emoji_reactions_hash = available_reactions.hash
self._available_emoji_reactions_fetched = time.monotonic()
self.log.debug(
"Got available emoji reactions: %s", self._available_emoji_reactions
)
elif self._available_emoji_reactions is None:
self.log.warning(
f"Got {available_reactions} in response to available reactions request"
" even though nothing is cached"
)
return self._available_emoji_reactions
def tl_to_json(self) -> Any:
+1 -1
View File
@@ -7,7 +7,7 @@ aiodns
brotli
#/qr_login
pillow>=4,<10
pillow>=10.0.1,<11
qrcode>=6,<8
#/formattednumbers
+3 -3
View File
@@ -3,8 +3,8 @@ python-magic>=0.4,<0.5
commonmark>=0.8,<0.10
aiohttp>=3,<4
yarl>=1,<2
mautrix>=0.20.0,<0.21
tulir-telethon==1.29.0a2
asyncpg>=0.20,<0.28
mautrix>=0.20.2,<0.21
tulir-telethon==1.30.0a2
asyncpg>=0.20,<0.29
mako>=1,<2
setuptools