Infinite backfill with MSC2716 (#817)
Disabled by default, with non-infinite fallback mode as the default behavior
This commit is contained in:
@@ -18,6 +18,7 @@ from __future__ import annotations
|
|||||||
from typing import cast
|
from typing import cast
|
||||||
import base64
|
import base64
|
||||||
import codecs
|
import codecs
|
||||||
|
import math
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from aiohttp import ClientSession, InvalidURL
|
from aiohttp import ClientSession, InvalidURL
|
||||||
@@ -427,6 +428,9 @@ async def backfill(evt: CommandEvent) -> None:
|
|||||||
if not evt.is_portal:
|
if not evt.is_portal:
|
||||||
await evt.reply("You can only use backfill in portal rooms")
|
await evt.reply("You can only use backfill in portal rooms")
|
||||||
return
|
return
|
||||||
|
elif not evt.config["bridge.backfill.enable"]:
|
||||||
|
await evt.reply("Backfilling is disabled in the bridge config")
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
limit = int(evt.args[0])
|
limit = int(evt.args[0])
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
@@ -435,16 +439,14 @@ async def backfill(evt: CommandEvent) -> None:
|
|||||||
if not evt.config["bridge.backfill.normal_groups"] and portal.peer_type == "chat":
|
if not evt.config["bridge.backfill.normal_groups"] and portal.peer_type == "chat":
|
||||||
await evt.reply("Backfilling normal groups is disabled in the bridge config")
|
await evt.reply("Backfilling normal groups is disabled in the bridge config")
|
||||||
return
|
return
|
||||||
try:
|
if portal.backfill_msc2716:
|
||||||
await portal.backfill(evt.sender, limit=limit)
|
messages_per_batch = evt.config["bridge.backfill.incremental.messages_per_batch"]
|
||||||
except TakeoutInitDelayError:
|
batches = math.ceil(limit / messages_per_batch)
|
||||||
msg = (
|
rounded = ""
|
||||||
"Please accept the data export request from a mobile device, "
|
if batches * messages_per_batch != limit:
|
||||||
"then re-run the backfill command."
|
rounded = f" (rounded message limit to {batches}*{messages_per_batch})"
|
||||||
)
|
await portal.enqueue_backfill(evt.sender, priority=0, max_batches=batches)
|
||||||
if portal.peer_type == "user":
|
await evt.reply(f"Backfill queued{rounded}")
|
||||||
from mautrix.appservice import IntentAPI
|
else:
|
||||||
|
output = await portal.forward_backfill(evt.sender, initial=False, override_limit=limit)
|
||||||
await portal.main_intent.send_notice(evt.room_id, msg)
|
await evt.reply(output)
|
||||||
else:
|
|
||||||
await evt.reply(msg)
|
|
||||||
|
|||||||
@@ -158,12 +158,18 @@ class Config(BaseBridgeConfig):
|
|||||||
copy("bridge.bridge_matrix_leave")
|
copy("bridge.bridge_matrix_leave")
|
||||||
copy("bridge.kick_on_logout")
|
copy("bridge.kick_on_logout")
|
||||||
copy("bridge.always_read_joined_telegram_notice")
|
copy("bridge.always_read_joined_telegram_notice")
|
||||||
copy("bridge.backfill.invite_own_puppet")
|
copy("bridge.backfill.enable")
|
||||||
copy("bridge.backfill.takeout_limit")
|
copy("bridge.backfill.msc2716")
|
||||||
copy("bridge.backfill.initial_limit")
|
copy("bridge.backfill.double_puppet_backfill")
|
||||||
copy("bridge.backfill.missed_limit")
|
|
||||||
copy("bridge.backfill.disable_notifications")
|
|
||||||
copy("bridge.backfill.normal_groups")
|
copy("bridge.backfill.normal_groups")
|
||||||
|
copy("bridge.backfill.forward.initial_limit")
|
||||||
|
copy("bridge.backfill.forward.sync_limit")
|
||||||
|
copy("bridge.backfill.incremental.messages_per_batch")
|
||||||
|
copy("bridge.backfill.incremental.post_batch_delay")
|
||||||
|
copy("bridge.backfill.incremental.max_batches.user")
|
||||||
|
copy("bridge.backfill.incremental.max_batches.normal_group")
|
||||||
|
copy("bridge.backfill.incremental.max_batches.supergroup")
|
||||||
|
copy("bridge.backfill.incremental.max_batches.channel")
|
||||||
|
|
||||||
copy("bridge.initial_power_level_overrides.group")
|
copy("bridge.initial_power_level_overrides.group")
|
||||||
copy("bridge.initial_power_level_overrides.user")
|
copy("bridge.initial_power_level_overrides.user")
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
from mautrix.util.async_db import Database
|
from mautrix.util.async_db import Database
|
||||||
|
|
||||||
|
from .backfill_queue import Backfill
|
||||||
from .bot_chat import BotChat
|
from .bot_chat import BotChat
|
||||||
from .disappearing_message import DisappearingMessage
|
from .disappearing_message import DisappearingMessage
|
||||||
from .message import Message
|
from .message import Message
|
||||||
@@ -38,6 +39,7 @@ def init(db: Database) -> None:
|
|||||||
BotChat,
|
BotChat,
|
||||||
PgSession,
|
PgSession,
|
||||||
DisappearingMessage,
|
DisappearingMessage,
|
||||||
|
Backfill,
|
||||||
):
|
):
|
||||||
table.db = db
|
table.db = db
|
||||||
|
|
||||||
@@ -54,4 +56,5 @@ __all__ = [
|
|||||||
"BotChat",
|
"BotChat",
|
||||||
"PgSession",
|
"PgSession",
|
||||||
"DisappearingMessage",
|
"DisappearingMessage",
|
||||||
|
"Backfill",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
# mautrix-telegram - A Matrix-Telegram puppeting bridge
|
||||||
|
# Copyright (C) 2022 Tulir Asokan, Sumner Evans
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, ClassVar
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from asyncpg import Record
|
||||||
|
from attr import dataclass
|
||||||
|
|
||||||
|
from mautrix.types import RoomID, UserID
|
||||||
|
from mautrix.util.async_db import Database
|
||||||
|
|
||||||
|
fake_db = Database.create("") if TYPE_CHECKING else None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Backfill:
|
||||||
|
db: ClassVar[Database] = fake_db
|
||||||
|
|
||||||
|
queue_id: int | None
|
||||||
|
user_mxid: UserID
|
||||||
|
priority: int
|
||||||
|
portal_tgid: int
|
||||||
|
portal_tg_receiver: int
|
||||||
|
messages_per_batch: int
|
||||||
|
post_batch_delay: int
|
||||||
|
max_batches: int
|
||||||
|
dispatch_time: datetime | None
|
||||||
|
completed_at: datetime | None
|
||||||
|
cooldown_timeout: datetime | None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def new(
|
||||||
|
user_mxid: UserID,
|
||||||
|
priority: int,
|
||||||
|
portal_tgid: int,
|
||||||
|
portal_tg_receiver: int,
|
||||||
|
messages_per_batch: int,
|
||||||
|
post_batch_delay: int = 0,
|
||||||
|
max_batches: int = -1,
|
||||||
|
) -> "Backfill":
|
||||||
|
return Backfill(
|
||||||
|
queue_id=None,
|
||||||
|
user_mxid=user_mxid,
|
||||||
|
priority=priority,
|
||||||
|
portal_tgid=portal_tgid,
|
||||||
|
portal_tg_receiver=portal_tg_receiver,
|
||||||
|
messages_per_batch=messages_per_batch,
|
||||||
|
post_batch_delay=post_batch_delay,
|
||||||
|
max_batches=max_batches,
|
||||||
|
dispatch_time=None,
|
||||||
|
completed_at=None,
|
||||||
|
cooldown_timeout=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_row(cls, row: Record | None) -> Backfill | None:
|
||||||
|
if row is None:
|
||||||
|
return None
|
||||||
|
return cls(**row)
|
||||||
|
|
||||||
|
columns = [
|
||||||
|
"user_mxid",
|
||||||
|
"priority",
|
||||||
|
"portal_tgid",
|
||||||
|
"portal_tg_receiver",
|
||||||
|
"messages_per_batch",
|
||||||
|
"post_batch_delay",
|
||||||
|
"max_batches",
|
||||||
|
"dispatch_time",
|
||||||
|
"completed_at",
|
||||||
|
"cooldown_timeout",
|
||||||
|
]
|
||||||
|
columns_str = ",".join(columns)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_next(cls, user_mxid: UserID) -> Backfill | None:
|
||||||
|
q = f"""
|
||||||
|
SELECT queue_id, {cls.columns_str}
|
||||||
|
FROM backfill_queue
|
||||||
|
WHERE user_mxid=$1
|
||||||
|
AND (
|
||||||
|
dispatch_time IS NULL
|
||||||
|
OR (
|
||||||
|
dispatch_time < $2
|
||||||
|
AND completed_at IS NULL
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
cooldown_timeout IS NULL
|
||||||
|
OR cooldown_timeout < current_timestamp
|
||||||
|
)
|
||||||
|
ORDER BY priority, queue_id
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
return cls._from_row(
|
||||||
|
await cls.db.fetchrow(q, user_mxid, datetime.now() - timedelta(minutes=15))
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get(
|
||||||
|
cls,
|
||||||
|
user_mxid: UserID,
|
||||||
|
portal_tgid: int,
|
||||||
|
portal_tg_receiver: int,
|
||||||
|
) -> Backfill | None:
|
||||||
|
q = f"""
|
||||||
|
SELECT queue_id, {cls.columns_str}
|
||||||
|
FROM backfill_queue
|
||||||
|
WHERE user_mxid=$1
|
||||||
|
AND portal_tgid=$2
|
||||||
|
AND portal_tg_receiver=$3
|
||||||
|
ORDER BY priority, queue_id
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
return cls._from_row(await cls.db.fetchrow(q, user_mxid, portal_tgid, portal_tg_receiver))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def delete_all(cls, user_mxid: UserID) -> None:
|
||||||
|
await cls.db.execute("DELETE FROM backfill_queue WHERE user_mxid=$1", user_mxid)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def delete_for_portal(cls, tgid: int, tg_receiver: int) -> None:
|
||||||
|
q = "DELETE FROM backfill_queue WHERE portal_tgid=$1 AND portal_tg_receiver=$2"
|
||||||
|
await cls.db.execute(q, tgid, tg_receiver)
|
||||||
|
|
||||||
|
async def insert(self) -> None:
|
||||||
|
q = f"""
|
||||||
|
INSERT INTO backfill_queue ({self.columns_str})
|
||||||
|
VALUES ({','.join(f'${i+1}' for i in range(len(self.columns)))})
|
||||||
|
RETURNING queue_id
|
||||||
|
"""
|
||||||
|
row = await self.db.fetchrow(
|
||||||
|
q,
|
||||||
|
self.user_mxid,
|
||||||
|
self.priority,
|
||||||
|
self.portal_tgid,
|
||||||
|
self.portal_tg_receiver,
|
||||||
|
self.messages_per_batch,
|
||||||
|
self.post_batch_delay,
|
||||||
|
self.max_batches,
|
||||||
|
self.dispatch_time,
|
||||||
|
self.completed_at,
|
||||||
|
self.cooldown_timeout,
|
||||||
|
)
|
||||||
|
self.queue_id = row["queue_id"]
|
||||||
|
|
||||||
|
async def mark_dispatched(self) -> None:
|
||||||
|
q = "UPDATE backfill_queue SET dispatch_time=$1 WHERE queue_id=$2"
|
||||||
|
await self.db.execute(q, datetime.now(), self.queue_id)
|
||||||
|
|
||||||
|
async def mark_done(self) -> None:
|
||||||
|
q = "UPDATE backfill_queue SET completed_at=$1 WHERE queue_id=$2"
|
||||||
|
await self.db.execute(q, datetime.now(), self.queue_id)
|
||||||
|
|
||||||
|
async def set_cooldown_timeout(self, timeout) -> None:
|
||||||
|
"""
|
||||||
|
Set the backfill request to cooldown for ``timeout`` seconds.
|
||||||
|
"""
|
||||||
|
q = "UPDATE backfill_queue SET cooldown_timeout=$1 WHERE queue_id=$2"
|
||||||
|
await self.db.execute(q, datetime.now() + timedelta(seconds=timeout), self.queue_id)
|
||||||
@@ -19,6 +19,7 @@ from typing import TYPE_CHECKING, ClassVar
|
|||||||
|
|
||||||
from asyncpg import Record
|
from asyncpg import Record
|
||||||
from attr import dataclass
|
from attr import dataclass
|
||||||
|
import attr
|
||||||
|
|
||||||
from mautrix.types import EventID, RoomID, UserID
|
from mautrix.types import EventID, RoomID, UserID
|
||||||
from mautrix.util.async_db import Database, Scheme
|
from mautrix.util.async_db import Database, Scheme
|
||||||
@@ -122,6 +123,14 @@ class Message:
|
|||||||
)
|
)
|
||||||
return cls._from_row(await cls.db.fetchrow(q, mx_room, tg_space))
|
return cls._from_row(await cls.db.fetchrow(q, mx_room, tg_space))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def find_first(cls, mx_room: RoomID, tg_space: TelegramID) -> Message | None:
|
||||||
|
q = (
|
||||||
|
f"SELECT {cls.columns} FROM message WHERE mx_room=$1 AND tg_space=$2 "
|
||||||
|
f"ORDER BY tgid ASC LIMIT 1"
|
||||||
|
)
|
||||||
|
return cls._from_row(await cls.db.fetchrow(q, mx_room, tg_space))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def delete_all(cls, mx_room: RoomID) -> None:
|
async def delete_all(cls, mx_room: RoomID) -> None:
|
||||||
await cls.db.execute("DELETE FROM message WHERE mx_room=$1", mx_room)
|
await cls.db.execute("DELETE FROM message WHERE mx_room=$1", mx_room)
|
||||||
@@ -173,6 +182,23 @@ class Message:
|
|||||||
q = "DELETE FROM message WHERE mxid=$1 AND mx_room=$2"
|
q = "DELETE FROM message WHERE mxid=$1 AND mx_room=$2"
|
||||||
await cls.db.execute(q, temp_mxid, mx_room)
|
await cls.db.execute(q, temp_mxid, mx_room)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def bulk_insert(cls, messages: list[Message]) -> None:
|
||||||
|
columns = cls.columns.split(", ")
|
||||||
|
records = [attr.astuple(message) for message in messages]
|
||||||
|
async with cls.db.acquire() as conn, conn.transaction():
|
||||||
|
if cls.db.scheme == Scheme.POSTGRES:
|
||||||
|
await conn.copy_records_to_table("message", records=records, columns=columns)
|
||||||
|
else:
|
||||||
|
await conn.executemany(cls._insert_query, records)
|
||||||
|
|
||||||
|
_insert_query: ClassVar[
|
||||||
|
str
|
||||||
|
] = """
|
||||||
|
INSERT INTO message (mxid, mx_room, tgid, tg_space, edit_index, redacted, content_hash, sender_mxid, sender)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _values(self):
|
def _values(self):
|
||||||
return (
|
return (
|
||||||
@@ -188,13 +214,7 @@ class Message:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def insert(self) -> None:
|
async def insert(self) -> None:
|
||||||
q = """
|
await self.db.execute(self._insert_query, *self._values)
|
||||||
INSERT INTO message (
|
|
||||||
mxid, mx_room, tgid, tg_space, edit_index, redacted, content_hash,
|
|
||||||
sender_mxid, sender
|
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
||||||
"""
|
|
||||||
await self.db.execute(q, *self._values)
|
|
||||||
|
|
||||||
async def delete(self) -> None:
|
async def delete(self) -> None:
|
||||||
q = "DELETE FROM message WHERE mxid=$1 AND mx_room=$2 AND tg_space=$3"
|
q = "DELETE FROM message WHERE mxid=$1 AND mx_room=$2 AND tg_space=$3"
|
||||||
|
|||||||
@@ -337,34 +337,49 @@ bridge:
|
|||||||
create_group_on_invite: true
|
create_group_on_invite: true
|
||||||
# Settings for backfilling messages from Telegram.
|
# Settings for backfilling messages from Telegram.
|
||||||
backfill:
|
backfill:
|
||||||
# Whether or not the Telegram ghosts of logged in Matrix users should be
|
# Allow backfilling at all?
|
||||||
# invited to private chats when backfilling history from Telegram. This is
|
enable: true
|
||||||
# usually needed to prevent rate limits and to allow timestamp massaging.
|
# Use MSC2716 for backfilling?
|
||||||
invite_own_puppet: true
|
|
||||||
# Maximum number of messages to backfill without using a takeout.
|
|
||||||
# The first time a takeout is used, the user has to manually approve it from a different
|
|
||||||
# device. If initial_limit or missed_limit are higher than this value, the bridge will ask
|
|
||||||
# the user to accept the takeout after logging in before syncing any chats.
|
|
||||||
takeout_limit: 100
|
|
||||||
# Maximum number of messages to backfill initially.
|
|
||||||
# Set to 0 to disable backfilling when creating portal, or -1 to disable the limit.
|
|
||||||
#
|
#
|
||||||
# N.B. Initial backfill will only start after member sync. Make sure your
|
# This requires a server with MSC2716 support, which is currently an experimental feature in Synapse.
|
||||||
# max_initial_member_sync is set to a low enough value so it doesn't take forever.
|
# It can be enabled by setting experimental_features -> msc2716_enabled to true in homeserver.yaml.
|
||||||
initial_limit: 0
|
msc2716: false
|
||||||
# Maximum number of messages to backfill if messages were missed while the bridge was
|
# Use double puppets for backfilling?
|
||||||
# disconnected. Note that this only works for logged in users and only if the chat isn't
|
#
|
||||||
# older than sync_update_limit
|
# If using MSC2716, the double puppets must be in the appservice's user ID namespace
|
||||||
# Set to 0 to disable backfilling missed messages.
|
# (because the bridge can't use the double puppet access token with batch sending).
|
||||||
missed_limit: 50
|
double_puppet_backfill: false
|
||||||
# If using double puppeting, should notifications be disabled
|
|
||||||
# while the initial backfill is in progress?
|
|
||||||
disable_notifications: false
|
|
||||||
# Whether or not to enable backfilling in normal groups.
|
# Whether or not to enable backfilling in normal groups.
|
||||||
# Normal groups have numerous technical problems in Telegram, and backfilling normal groups
|
# Normal groups have numerous technical problems in Telegram, and backfilling normal groups
|
||||||
# will likely cause problems if there are multiple Matrix users in the group.
|
# will likely cause problems if there are multiple Matrix users in the group.
|
||||||
normal_groups: false
|
normal_groups: false
|
||||||
|
|
||||||
|
# Forward backfilling limits. These apply to both MSC2716 and legacy backfill.
|
||||||
|
forward:
|
||||||
|
# Number of messages to backfill immediately after creating a portal.
|
||||||
|
initial_limit: 10
|
||||||
|
# Number of messages to backfill when syncing chats.
|
||||||
|
sync_limit: 100
|
||||||
|
|
||||||
|
# Settings for incremental backfill of history. These only apply when using MSC2716.
|
||||||
|
incremental:
|
||||||
|
# Maximum number of messages to backfill per batch.
|
||||||
|
messages_per_batch: 100
|
||||||
|
# The number of seconds to wait after backfilling the batch of messages.
|
||||||
|
post_batch_delay: 20
|
||||||
|
# The maximum number of batches to backfill per portal, split by the chat type.
|
||||||
|
# If set to -1, all messages in the chat will eventually be backfilled.
|
||||||
|
max_batches:
|
||||||
|
# Direct chats
|
||||||
|
user: -1
|
||||||
|
# Normal groups. Note that the normal_groups option above must be enabled
|
||||||
|
# for these to be backfilled.
|
||||||
|
normal_group: -1
|
||||||
|
# Supergroups
|
||||||
|
supergroup: 10
|
||||||
|
# Broadcast channels
|
||||||
|
channel: -1
|
||||||
|
|
||||||
# Overrides for base power levels.
|
# Overrides for base power levels.
|
||||||
initial_power_level_overrides:
|
initial_power_level_overrides:
|
||||||
user: {}
|
user: {}
|
||||||
|
|||||||
@@ -16,15 +16,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
import sys
|
||||||
|
|
||||||
from mautrix.bridge import BaseMatrixHandler
|
from mautrix.bridge import BaseMatrixHandler
|
||||||
from mautrix.errors import MatrixError
|
|
||||||
from mautrix.types import (
|
from mautrix.types import (
|
||||||
Event,
|
Event,
|
||||||
EventID,
|
EventID,
|
||||||
EventType,
|
EventType,
|
||||||
MemberStateEventContent,
|
MemberStateEventContent,
|
||||||
MessageType,
|
|
||||||
PresenceEvent,
|
PresenceEvent,
|
||||||
PresenceState,
|
PresenceState,
|
||||||
ReactionEvent,
|
ReactionEvent,
|
||||||
@@ -36,7 +35,6 @@ from mautrix.types import (
|
|||||||
RoomTopicStateEventContent as TopicContent,
|
RoomTopicStateEventContent as TopicContent,
|
||||||
SingleReceiptEventContent,
|
SingleReceiptEventContent,
|
||||||
StateEvent,
|
StateEvent,
|
||||||
TextMessageEventContent,
|
|
||||||
TypingEvent,
|
TypingEvent,
|
||||||
UserID,
|
UserID,
|
||||||
)
|
)
|
||||||
@@ -63,6 +61,21 @@ class MatrixHandler(BaseMatrixHandler):
|
|||||||
|
|
||||||
self._previously_typing = {}
|
self._previously_typing = {}
|
||||||
|
|
||||||
|
async def check_versions(self) -> None:
|
||||||
|
await super().check_versions()
|
||||||
|
if self.config["bridge.backfill.msc2716"] and not (
|
||||||
|
support := self.versions.supports("org.matrix.msc2716")
|
||||||
|
):
|
||||||
|
self.log.fatal(
|
||||||
|
"Backfilling with MSC2716 is enabled in bridge config, but "
|
||||||
|
+ (
|
||||||
|
"batch sending is not enabled on homeserver"
|
||||||
|
if support is False
|
||||||
|
else "homeserver does not support batch sending"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
sys.exit(18)
|
||||||
|
|
||||||
async def handle_puppet_group_invite(
|
async def handle_puppet_group_invite(
|
||||||
self,
|
self,
|
||||||
room_id: RoomID,
|
room_id: RoomID,
|
||||||
|
|||||||
+354
-148
@@ -132,12 +132,15 @@ from telethon.tl.types import (
|
|||||||
UserProfilePhotoEmpty,
|
UserProfilePhotoEmpty,
|
||||||
)
|
)
|
||||||
from telethon.utils import encode_waveform
|
from telethon.utils import encode_waveform
|
||||||
|
import attr
|
||||||
|
|
||||||
from mautrix.appservice import DOUBLE_PUPPET_SOURCE_KEY, IntentAPI
|
from mautrix.appservice import DOUBLE_PUPPET_SOURCE_KEY, IntentAPI
|
||||||
from mautrix.bridge import BasePortal, NotificationDisabler, RejectMatrixInvite, async_getter_lock
|
from mautrix.bridge import BasePortal, NotificationDisabler, RejectMatrixInvite, async_getter_lock
|
||||||
from mautrix.errors import IntentError, MatrixRequestError, MForbidden
|
from mautrix.errors import IntentError, MatrixRequestError, MForbidden
|
||||||
from mautrix.types import (
|
from mautrix.types import (
|
||||||
BatchID,
|
BatchID,
|
||||||
|
BatchSendEvent,
|
||||||
|
BatchSendStateEvent,
|
||||||
BeeperMessageStatusEventContent,
|
BeeperMessageStatusEventContent,
|
||||||
ContentURI,
|
ContentURI,
|
||||||
EventID,
|
EventID,
|
||||||
@@ -148,6 +151,7 @@ from mautrix.types import (
|
|||||||
LocationMessageEventContent,
|
LocationMessageEventContent,
|
||||||
MediaMessageEventContent,
|
MediaMessageEventContent,
|
||||||
Membership,
|
Membership,
|
||||||
|
MemberStateEventContent,
|
||||||
MessageEventContent,
|
MessageEventContent,
|
||||||
MessageStatus,
|
MessageStatus,
|
||||||
MessageStatusReason,
|
MessageStatusReason,
|
||||||
@@ -182,6 +186,7 @@ from . import (
|
|||||||
)
|
)
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .db import (
|
from .db import (
|
||||||
|
Backfill,
|
||||||
DisappearingMessage,
|
DisappearingMessage,
|
||||||
Message as DBMessage,
|
Message as DBMessage,
|
||||||
Portal as DBPortal,
|
Portal as DBPortal,
|
||||||
@@ -204,6 +209,7 @@ if TYPE_CHECKING:
|
|||||||
StateBridge = EventType.find("m.bridge", EventType.Class.STATE)
|
StateBridge = EventType.find("m.bridge", EventType.Class.STATE)
|
||||||
StateHalfShotBridge = EventType.find("uk.half-shot.bridge", EventType.Class.STATE)
|
StateHalfShotBridge = EventType.find("uk.half-shot.bridge", EventType.Class.STATE)
|
||||||
DummyPortalCreated = EventType.find("fi.mau.dummy.portal_created", EventType.Class.MESSAGE)
|
DummyPortalCreated = EventType.find("fi.mau.dummy.portal_created", EventType.Class.MESSAGE)
|
||||||
|
StateMarker = EventType.find("org.matrix.msc2716.marker", EventType.Class.STATE)
|
||||||
|
|
||||||
InviteList = Union[UserID, List[UserID]]
|
InviteList = Union[UserID, List[UserID]]
|
||||||
UpdateTyping = Union[UpdateUserTyping, UpdateChatUserTyping, UpdateChannelUserTyping]
|
UpdateTyping = Union[UpdateUserTyping, UpdateChatUserTyping, UpdateChannelUserTyping]
|
||||||
@@ -250,6 +256,7 @@ class Portal(DBPortal, BasePortal):
|
|||||||
backfill_lock: SimpleLock
|
backfill_lock: SimpleLock
|
||||||
backfill_method_lock: asyncio.Lock
|
backfill_method_lock: asyncio.Lock
|
||||||
backfill_leave: set[IntentAPI] | None
|
backfill_leave: set[IntentAPI] | None
|
||||||
|
backfill_msc2716: bool
|
||||||
|
|
||||||
alias: RoomAlias | None
|
alias: RoomAlias | None
|
||||||
|
|
||||||
@@ -269,6 +276,7 @@ class Portal(DBPortal, BasePortal):
|
|||||||
_sponsored_seen: dict[UserID, bool]
|
_sponsored_seen: dict[UserID, bool]
|
||||||
_new_messages_after_sponsored: bool
|
_new_messages_after_sponsored: bool
|
||||||
|
|
||||||
|
_member_list_cache: dict[EventID, set[UserID]]
|
||||||
_prev_reaction_poll: dict[UserID, float]
|
_prev_reaction_poll: dict[UserID, float]
|
||||||
|
|
||||||
_msg_conv: putil.TelegramMessageConverter
|
_msg_conv: putil.TelegramMessageConverter
|
||||||
@@ -322,6 +330,7 @@ class Portal(DBPortal, BasePortal):
|
|||||||
self.log = self.log.getChild(self.tgid_log if self.tgid else self.mxid)
|
self.log = self.log.getChild(self.tgid_log if self.tgid else self.mxid)
|
||||||
self._main_intent = None
|
self._main_intent = None
|
||||||
self.deleted = False
|
self.deleted = False
|
||||||
|
|
||||||
self.backfill_lock = SimpleLock(
|
self.backfill_lock = SimpleLock(
|
||||||
"Waiting for backfilling to finish before handling %s", log=self.log
|
"Waiting for backfilling to finish before handling %s", log=self.log
|
||||||
)
|
)
|
||||||
@@ -341,6 +350,7 @@ class Portal(DBPortal, BasePortal):
|
|||||||
self._new_messages_after_sponsored = True
|
self._new_messages_after_sponsored = True
|
||||||
self._bridging_blocked_at_runtime = False
|
self._bridging_blocked_at_runtime = False
|
||||||
|
|
||||||
|
self._member_list_cache = {}
|
||||||
self._prev_reaction_poll = defaultdict(lambda: 0.0)
|
self._prev_reaction_poll = defaultdict(lambda: 0.0)
|
||||||
|
|
||||||
self._msg_conv = putil.TelegramMessageConverter(self)
|
self._msg_conv = putil.TelegramMessageConverter(self)
|
||||||
@@ -428,6 +438,7 @@ class Portal(DBPortal, BasePortal):
|
|||||||
cls.filter_mode = cls.config["bridge.filter.mode"]
|
cls.filter_mode = cls.config["bridge.filter.mode"]
|
||||||
cls.filter_list = cls.config["bridge.filter.list"]
|
cls.filter_list = cls.config["bridge.filter.list"]
|
||||||
cls.hs_domain = cls.config["homeserver.domain"]
|
cls.hs_domain = cls.config["homeserver.domain"]
|
||||||
|
cls.backfill_msc2716 = cls.config["bridge.backfill.msc2716"]
|
||||||
cls.alias_template = SimpleTemplate(
|
cls.alias_template = SimpleTemplate(
|
||||||
cls.config["bridge.alias_template"],
|
cls.config["bridge.alias_template"],
|
||||||
"groupname",
|
"groupname",
|
||||||
@@ -905,32 +916,30 @@ class Portal(DBPortal, BasePortal):
|
|||||||
|
|
||||||
self.mxid = room_id
|
self.mxid = room_id
|
||||||
self.by_mxid[self.mxid] = self
|
self.by_mxid[self.mxid] = self
|
||||||
self.first_event_id = await self.main_intent.send_message_event(
|
|
||||||
self.mxid, DummyPortalCreated, {}
|
|
||||||
)
|
|
||||||
await self.save()
|
await self.save()
|
||||||
self.log.debug(f"Matrix room created: {self.mxid}")
|
self.log.debug(f"Matrix room created: {self.mxid}")
|
||||||
await self.az.state_store.set_power_levels(self.mxid, power_levels)
|
await self.az.state_store.set_power_levels(self.mxid, power_levels)
|
||||||
await user.register_portal(self)
|
await user.register_portal(self)
|
||||||
|
|
||||||
await self.invite_to_matrix(invites)
|
await self.invite_to_matrix(invites)
|
||||||
|
await self.update_matrix_room(user, entity, puppet, levels=power_levels, users=users)
|
||||||
|
|
||||||
update_room = asyncio.create_task(
|
self.first_event_id = await self.main_intent.send_message_event(
|
||||||
self.update_matrix_room(user, entity, puppet, levels=power_levels, users=users)
|
self.mxid, DummyPortalCreated, {}
|
||||||
)
|
)
|
||||||
|
if not self.bridge.homeserver_software.is_hungry:
|
||||||
if self.config["bridge.backfill.initial_limit"] > 0:
|
self._member_list_cache[self.first_event_id] = set(
|
||||||
self.log.debug(
|
(await self.main_intent.get_joined_members(self.mxid)).keys()
|
||||||
"Initial backfill is enabled. Waiting for room members to sync "
|
|
||||||
"and then starting backfill"
|
|
||||||
)
|
)
|
||||||
await update_room
|
await self.save()
|
||||||
|
|
||||||
|
if isinstance(user, u.User) or not self.backfill_msc2716:
|
||||||
try:
|
try:
|
||||||
if isinstance(user, u.User):
|
await self.forward_backfill(user, initial=True)
|
||||||
await self.backfill(user, is_initial=True)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log.exception("Failed to backfill new portal")
|
self.log.exception("Error in initial backfill")
|
||||||
|
if self.backfill_msc2716:
|
||||||
|
await self.enqueue_backfill(user, priority=50)
|
||||||
|
|
||||||
return self.mxid
|
return self.mxid
|
||||||
|
|
||||||
@@ -2595,142 +2604,188 @@ class Portal(DBPortal, BasePortal):
|
|||||||
await DBMessage.replace_temp_mxid(temporary_identifier, self.mxid, event_id)
|
await DBMessage.replace_temp_mxid(temporary_identifier, self.mxid, event_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _takeout_options(self) -> dict[str, bool | int]:
|
def _default_max_batches(self) -> int:
|
||||||
return {
|
if self.peer_type == "user":
|
||||||
"files": True,
|
own_type = "user"
|
||||||
"megagroups": self.megagroup,
|
elif self.peer_type == "chat":
|
||||||
"chats": self.peer_type == "chat",
|
own_type = "normal_group"
|
||||||
"users": self.peer_type == "user",
|
elif self.megagroup:
|
||||||
"channels": (self.peer_type == "channel" and not self.megagroup),
|
own_type = "supergroup"
|
||||||
"max_file_size": min(self.matrix.media_config.upload_size, 2000 * 1024 * 1024),
|
else:
|
||||||
}
|
own_type = "channel"
|
||||||
|
return self.config[f"bridge.backfill.incremental.max_batches.{own_type}"]
|
||||||
|
|
||||||
|
async def enqueue_backfill(
|
||||||
|
self,
|
||||||
|
source: u.User,
|
||||||
|
priority: int,
|
||||||
|
max_batches: int | None = None,
|
||||||
|
messages_per_batch: int | None = None,
|
||||||
|
) -> None:
|
||||||
|
# TODO check that there are no queued backfills
|
||||||
|
# if not await Backfill.get(source.mxid, self.tgid, self.tg_receiver):
|
||||||
|
await Backfill.new(
|
||||||
|
user_mxid=source.mxid,
|
||||||
|
priority=priority,
|
||||||
|
portal_tgid=self.tgid,
|
||||||
|
portal_tg_receiver=self.tg_receiver,
|
||||||
|
messages_per_batch=(
|
||||||
|
messages_per_batch or self.config["bridge.backfill.incremental.messages_per_batch"]
|
||||||
|
),
|
||||||
|
post_batch_delay=self.config["bridge.backfill.incremental.post_batch_delay"],
|
||||||
|
max_batches=max_batches or self._default_max_batches,
|
||||||
|
).insert()
|
||||||
|
source.wakeup_backfill_task.set()
|
||||||
|
|
||||||
|
async def forward_backfill(
|
||||||
|
self,
|
||||||
|
source: u.User,
|
||||||
|
initial: bool,
|
||||||
|
last_tgid: int | None = None,
|
||||||
|
override_limit: int | None = None,
|
||||||
|
) -> str:
|
||||||
|
type = "initial" if initial else "sync"
|
||||||
|
limit = override_limit or self.config[f"bridge.backfill.forward.{type}_limit"]
|
||||||
|
with self.backfill_lock:
|
||||||
|
output = await self.backfill(
|
||||||
|
source, source.client, forward=True, forward_limit=limit, last_tgid=last_tgid
|
||||||
|
)
|
||||||
|
self.log.debug(f"Forward backfill complete, status: {output}")
|
||||||
|
return output
|
||||||
|
|
||||||
async def backfill(
|
async def backfill(
|
||||||
self,
|
self,
|
||||||
source: u.User,
|
source: u.User,
|
||||||
is_initial: bool = False,
|
client: MautrixTelegramClient,
|
||||||
limit: int | None = None,
|
req: Backfill | None = None,
|
||||||
last_id: int | None = None,
|
forward: bool = False,
|
||||||
) -> None:
|
forward_limit: int | None = None,
|
||||||
|
last_tgid: int | None = None,
|
||||||
|
) -> str:
|
||||||
async with self.backfill_method_lock:
|
async with self.backfill_method_lock:
|
||||||
await self._locked_backfill(source, is_initial, limit, last_id)
|
return await self._locked_backfill(
|
||||||
|
source, client, req, forward, forward_limit, last_tgid
|
||||||
|
)
|
||||||
|
|
||||||
async def _locked_backfill(
|
async def _locked_backfill(
|
||||||
self,
|
self,
|
||||||
source: u.User,
|
source: u.User,
|
||||||
is_initial: bool = False,
|
client: MautrixTelegramClient,
|
||||||
limit: int | None = None,
|
req: Backfill | None = None,
|
||||||
|
forward: bool = False,
|
||||||
|
forward_limit: int | None = None,
|
||||||
last_tgid: int | None = None,
|
last_tgid: int | None = None,
|
||||||
) -> None:
|
) -> str:
|
||||||
limit = limit or (
|
assert forward != bool(req)
|
||||||
self.config["bridge.backfill.initial_limit"]
|
|
||||||
if is_initial
|
|
||||||
else self.config["bridge.backfill.missed_limit"]
|
|
||||||
)
|
|
||||||
if limit == 0:
|
|
||||||
return
|
|
||||||
if not self.config["bridge.backfill.normal_groups"] and self.peer_type == "chat":
|
if not self.config["bridge.backfill.normal_groups"] and self.peer_type == "chat":
|
||||||
return
|
return "Backfilling normal groups is disabled in the bridge config"
|
||||||
last_in_room = await DBMessage.find_last(
|
tg_space = source.tgid if self.peer_type != "channel" else self.tgid
|
||||||
self.mxid, (source.tgid if self.peer_type != "channel" else self.tgid)
|
prev_event_id = self.first_event_id
|
||||||
)
|
if forward:
|
||||||
min_id = last_in_room.tgid if last_in_room else 0
|
last_in_room = await DBMessage.find_last(self.mxid, tg_space)
|
||||||
if last_tgid is None:
|
if last_in_room:
|
||||||
messages = await source.client.get_messages(self.peer, limit=1)
|
prev_event_id = last_in_room.mxid
|
||||||
if not messages:
|
min_id = last_in_room.tgid
|
||||||
# The chat seems empty
|
|
||||||
return
|
|
||||||
last_tgid = messages[0].id
|
|
||||||
if last_tgid <= min_id or (last_tgid == 1 and self.peer_type == "channel"):
|
|
||||||
# Nothing to backfill
|
|
||||||
return
|
|
||||||
if limit < 0:
|
|
||||||
limit = last_tgid - min_id
|
|
||||||
limit_type = "unlimited"
|
|
||||||
elif self.peer_type == "channel":
|
|
||||||
min_id = max(last_tgid - limit, min_id)
|
|
||||||
# This is now just an approximate message count, not the actual limit.
|
|
||||||
limit = last_tgid - min_id
|
|
||||||
limit_type = "channel"
|
|
||||||
else:
|
|
||||||
# This limit will be higher than the actual message count if there are any messages
|
|
||||||
# in other DMs or normal groups, but that's not too bad.
|
|
||||||
limit = min(last_tgid - min_id, limit)
|
|
||||||
limit_type = "dm/minigroup"
|
|
||||||
self.log.debug(
|
|
||||||
f"Backfilling up to {limit} messages after ID {min_id} through {source.mxid} "
|
|
||||||
f"(last message: {last_tgid}, limit type: {limit_type})"
|
|
||||||
)
|
|
||||||
with self.backfill_lock:
|
|
||||||
await self._backfill(source, min_id, limit)
|
|
||||||
|
|
||||||
async def _backfill(self, source: u.User, min_id: int, limit: int) -> None:
|
|
||||||
self.backfill_leave = set()
|
|
||||||
if (
|
|
||||||
self.peer_type == "user"
|
|
||||||
and self.tgid != source.tgid
|
|
||||||
and self.config["bridge.backfill.invite_own_puppet"]
|
|
||||||
):
|
|
||||||
self.log.debug("Adding %s's default puppet to room for backfilling", source.mxid)
|
|
||||||
sender = await p.Puppet.get_by_tgid(source.tgid)
|
|
||||||
await self.main_intent.invite_user(self.mxid, sender.default_mxid)
|
|
||||||
await sender.default_mxid_intent.join_room_by_id(self.mxid)
|
|
||||||
self.backfill_leave.add(sender.default_mxid_intent)
|
|
||||||
|
|
||||||
client = source.client
|
|
||||||
async with NotificationDisabler(self.mxid, source):
|
|
||||||
if limit > self.config["bridge.backfill.takeout_limit"]:
|
|
||||||
self.log.debug(f"Opening takeout client for {source.tgid}")
|
|
||||||
async with client.takeout(**self._takeout_options) as takeout:
|
|
||||||
count, handled = await self._backfill_messages(source, min_id, limit, takeout)
|
|
||||||
else:
|
else:
|
||||||
count, handled = await self._backfill_messages(source, min_id, limit, client)
|
min_id = 0
|
||||||
|
if last_tgid is None:
|
||||||
for intent in self.backfill_leave:
|
messages = await source.client.get_messages(self.peer, limit=1)
|
||||||
self.log.trace("Leaving room with %s post-backfill", intent.mxid)
|
if not messages:
|
||||||
await intent.leave_room(self.mxid)
|
return "Chat is empty, nothing to backfill"
|
||||||
self.backfill_leave = None
|
last_tgid = messages[0].id
|
||||||
self.log.info(
|
if last_tgid <= min_id or (last_tgid == 1 and self.peer_type == "channel"):
|
||||||
"Backfilled %d (of %d fetched) messages through %s", handled, count, source.mxid
|
return (
|
||||||
)
|
f"Last bridged message {min_id} is equal to or greater than last message "
|
||||||
|
f"in Telegram chat {last_tgid}, nothing to backfill"
|
||||||
async def _backfill_messages(
|
)
|
||||||
self, source: u.User, min_id: int, limit: int, client: MautrixTelegramClient
|
limit = last_tgid - min_id
|
||||||
) -> tuple[int, int]:
|
if (forward_limit or 0) > 0:
|
||||||
count = handled_count = 0
|
limit = min(limit, forward_limit)
|
||||||
entity = await self.get_input_entity(source)
|
|
||||||
if self.peer_type == "channel":
|
|
||||||
# This is a channel or supergroup, so we'll backfill messages based on the ID.
|
|
||||||
# There are some cases, such as deleted messages, where this may backfill less
|
|
||||||
# messages than the limit.
|
|
||||||
self.log.debug(f"Iterating all messages starting with {min_id} (approx: {limit})")
|
|
||||||
messages = client.iter_messages(entity, reverse=True, min_id=min_id)
|
|
||||||
async for message in messages:
|
|
||||||
count += 1
|
|
||||||
was_handled = await self._handle_telegram_backfill_message(source, message)
|
|
||||||
handled_count += 1 if was_handled else 0
|
|
||||||
else:
|
|
||||||
# Private chats and normal groups don't have their own message ID namespace,
|
|
||||||
# which means we'll have to fetch messages a different way.
|
|
||||||
self.log.debug(
|
self.log.debug(
|
||||||
f"Fetching up to {limit} most recent messages, ignoring anything before {min_id}"
|
f"Backfilling up to {limit} messages after ID {min_id} through {source.mxid} "
|
||||||
|
f"(last message: {last_tgid})"
|
||||||
)
|
)
|
||||||
messages = await client.get_messages(entity, min_id=min_id, limit=limit)
|
anchor_id = min_id
|
||||||
for message in reversed(messages):
|
else:
|
||||||
count += 1
|
limit = req.messages_per_batch
|
||||||
if message.id <= min_id:
|
first_in_room = await DBMessage.find_first(self.mxid, tg_space)
|
||||||
self.log.trace(
|
anchor_id = first_in_room.tgid if first_in_room else None
|
||||||
f"Skipping {message.id} in backfill response as it's lower than "
|
self.log.debug(
|
||||||
f"the last bridged message ({min_id})"
|
f"Backfilling up to {req.messages_per_batch} historical messages "
|
||||||
)
|
f"before {anchor_id} through {source.mxid}"
|
||||||
continue
|
)
|
||||||
was_handled = await self._handle_telegram_backfill_message(source, message)
|
insertion_id, message_count, first_id = await self._backfill_messages(
|
||||||
handled_count += 1 if was_handled else 0
|
source, client, forward, anchor_id, limit, prev_event_id
|
||||||
return count, handled_count
|
)
|
||||||
|
if prev_event_id == self.first_event_id:
|
||||||
|
if insertion_id and not self.base_insertion_id:
|
||||||
|
self.base_insertion_id = insertion_id
|
||||||
|
elif not insertion_id:
|
||||||
|
insertion_id = self.base_insertion_id
|
||||||
|
await self.save()
|
||||||
|
# TODO this should probably check actual event count instead of message count
|
||||||
|
if message_count > 0 and self.backfill_msc2716:
|
||||||
|
await self.main_intent.send_state_event(
|
||||||
|
self.mxid,
|
||||||
|
StateMarker,
|
||||||
|
{
|
||||||
|
"org.matrix.msc2716.marker.insertion": insertion_id,
|
||||||
|
"com.beeper.timestamp": int(time.time() * 1000),
|
||||||
|
},
|
||||||
|
state_key=insertion_id,
|
||||||
|
)
|
||||||
|
if forward:
|
||||||
|
self.log.debug(f"Forward backfill finished with {message_count} messages")
|
||||||
|
elif message_count > 0 and first_id and first_id > 1:
|
||||||
|
if req.max_batches in (0, 1):
|
||||||
|
return "Already backfilled enough batches, not enqueuing more"
|
||||||
|
self.log.debug(f"Enqueuing more backfill through {source.mxid}")
|
||||||
|
await self.enqueue_backfill(
|
||||||
|
source,
|
||||||
|
priority=100,
|
||||||
|
messages_per_batch=req.messages_per_batch,
|
||||||
|
max_batches=-1 if req.max_batches < 0 else (req.max_batches - 1),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.log.debug("No more messages to backfill")
|
||||||
|
return f"Backfilled {message_count} messages"
|
||||||
|
|
||||||
async def _handle_telegram_backfill_message(
|
def _can_double_puppet_backfill(self, custom_mxid: UserID) -> bool:
|
||||||
self, source: au.AbstractUser, msg: Message | MessageService
|
if not self.backfill_msc2716:
|
||||||
) -> bool:
|
return True
|
||||||
|
if not self.config["bridge.backfill.double_puppet_backfill"]:
|
||||||
|
return False
|
||||||
|
if self.bridge.homeserver_software.is_hungry:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Batch sending can only use local users, so don't allow double puppets on other servers.
|
||||||
|
if custom_mxid[custom_mxid.index(":") + 1 :] != self.config["homeserver.domain"]:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def _get_members_at(self, event_id: EventID) -> set[UserID]:
|
||||||
|
try:
|
||||||
|
return self._member_list_cache[event_id]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
# TODO cache the list in db?
|
||||||
|
self.log.debug(f"Fetching member list at {event_id}")
|
||||||
|
ctx = await self.main_intent.get_event_context(self.mxid, event_id, limit=0)
|
||||||
|
members = {
|
||||||
|
evt.state_key
|
||||||
|
for evt in ctx.state
|
||||||
|
if evt.type == EventType.ROOM_MEMBER and evt.content.membership == Membership.JOIN
|
||||||
|
}
|
||||||
|
self.log.debug(f"Found {len(members)} members at {event_id}")
|
||||||
|
self._member_list_cache[event_id] = members
|
||||||
|
return members
|
||||||
|
|
||||||
|
async def _convert_batch_msg(
|
||||||
|
self,
|
||||||
|
source: u.User,
|
||||||
|
msg: Message,
|
||||||
|
add_member: Callable[[IntentAPI, str, ContentURI], Awaitable[None]],
|
||||||
|
) -> tuple[putil.ConvertedMessage, IntentAPI]:
|
||||||
if msg.from_id and isinstance(msg.from_id, (PeerUser, PeerChannel)):
|
if msg.from_id and isinstance(msg.from_id, (PeerUser, PeerChannel)):
|
||||||
sender = await p.Puppet.get_by_peer(msg.from_id)
|
sender = await p.Puppet.get_by_peer(msg.from_id)
|
||||||
elif isinstance(msg.peer_id, PeerUser):
|
elif isinstance(msg.peer_id, PeerUser):
|
||||||
@@ -2740,20 +2795,171 @@ class Portal(DBPortal, BasePortal):
|
|||||||
sender = await p.Puppet.get_by_peer(msg.peer_id)
|
sender = await p.Puppet.get_by_peer(msg.peer_id)
|
||||||
else:
|
else:
|
||||||
sender = None
|
sender = None
|
||||||
if isinstance(msg, MessageService):
|
if sender:
|
||||||
if isinstance(msg.action, MessageActionContactSignUp):
|
intent = sender.intent_for(self)
|
||||||
await self.handle_telegram_joined(source, sender, msg, backfill=True)
|
if not sender.displayname:
|
||||||
return True
|
entity = await source.client.get_entity(sender.peer)
|
||||||
else:
|
await sender.update_info(source, entity)
|
||||||
self.log.debug(
|
|
||||||
f"Unhandled service message {type(msg.action).__name__} in backfill"
|
|
||||||
)
|
|
||||||
elif isinstance(msg, Message):
|
|
||||||
await self.handle_telegram_message(source, sender, msg)
|
|
||||||
return True
|
|
||||||
else:
|
else:
|
||||||
self.log.debug(f"Unhandled message type {type(msg).__name__} in backfill")
|
intent = self.main_intent
|
||||||
return False
|
if intent.api.is_real_user and not self._can_double_puppet_backfill(intent.mxid):
|
||||||
|
intent = sender.default_mxid_intent
|
||||||
|
if sender:
|
||||||
|
await add_member(intent, sender.displayname, sender.avatar_url)
|
||||||
|
is_bot = sender.is_bot if sender else False
|
||||||
|
converted = await self._msg_conv.convert(source, intent, is_bot, msg)
|
||||||
|
return converted, intent
|
||||||
|
|
||||||
|
async def _wrap_batch_msg(
|
||||||
|
self,
|
||||||
|
intent: IntentAPI,
|
||||||
|
msg: Message,
|
||||||
|
converted: putil.ConvertedMessage,
|
||||||
|
caption: bool = False,
|
||||||
|
) -> BatchSendEvent:
|
||||||
|
if caption:
|
||||||
|
content = converted.caption
|
||||||
|
event_type = EventType.ROOM_MESSAGE
|
||||||
|
else:
|
||||||
|
content = converted.content
|
||||||
|
event_type = converted.type
|
||||||
|
if self.encrypted and self.matrix.e2ee:
|
||||||
|
event_type, content = await self.matrix.e2ee.encrypt(self.mxid, event_type, content)
|
||||||
|
return BatchSendEvent(
|
||||||
|
sender=intent.mxid,
|
||||||
|
timestamp=int(msg.date.timestamp() * 1000),
|
||||||
|
content=content,
|
||||||
|
type=event_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _backfill_messages(
|
||||||
|
self,
|
||||||
|
source: u.User,
|
||||||
|
client: MautrixTelegramClient,
|
||||||
|
forward: bool,
|
||||||
|
anchor_id: int,
|
||||||
|
limit: int,
|
||||||
|
prev_event_id: EventID,
|
||||||
|
) -> tuple[EventID | None, int, int]:
|
||||||
|
entity = await self.get_input_entity(source)
|
||||||
|
events = []
|
||||||
|
intents = []
|
||||||
|
metas = []
|
||||||
|
state_events = []
|
||||||
|
do_batch_send = self.backfill_msc2716
|
||||||
|
added_members = (
|
||||||
|
await self._get_members_at(prev_event_id)
|
||||||
|
if not self.bridge.homeserver_software.is_hungry and do_batch_send
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
before_first_msg_timestamp = 0
|
||||||
|
|
||||||
|
async def add_member(intent: IntentAPI, displayname: str, avatar_url: ContentURI) -> None:
|
||||||
|
if self.bridge.homeserver_software.is_hungry or intent.mxid in added_members:
|
||||||
|
return
|
||||||
|
added_members.add(intent.mxid)
|
||||||
|
if not do_batch_send:
|
||||||
|
# TODO leave these members?
|
||||||
|
await intent.ensure_joined(self.mxid)
|
||||||
|
return
|
||||||
|
invite_event = BatchSendStateEvent(
|
||||||
|
type=EventType.ROOM_MEMBER,
|
||||||
|
state_key=intent.mxid,
|
||||||
|
sender=self.main_intent.mxid,
|
||||||
|
timestamp=before_first_msg_timestamp,
|
||||||
|
content=MemberStateEventContent(
|
||||||
|
membership=Membership.INVITE,
|
||||||
|
displayname=displayname,
|
||||||
|
avatar_url=avatar_url,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
join_event = attr.evolve(
|
||||||
|
invite_event,
|
||||||
|
content=attr.evolve(invite_event.content, membership=Membership.JOIN),
|
||||||
|
sender=intent.mxid,
|
||||||
|
)
|
||||||
|
state_events.append(invite_event)
|
||||||
|
state_events.append(join_event)
|
||||||
|
|
||||||
|
first_id = anchor_id
|
||||||
|
message_count = 0
|
||||||
|
minmax = {"min_id": anchor_id} if forward else {"max_id": anchor_id}
|
||||||
|
if not forward and not anchor_id:
|
||||||
|
anchor_id = 2**31 - 1
|
||||||
|
minmax = {}
|
||||||
|
self.log.debug(f"Iterating messages through {source.tgid} with {limit=}, {minmax}")
|
||||||
|
# Iterate messages newest to oldest and collect the results
|
||||||
|
async for msg in client.iter_messages(entity, limit=limit, **minmax):
|
||||||
|
message_count += 1
|
||||||
|
if (forward and msg.id <= anchor_id) or (not forward and msg.id >= anchor_id):
|
||||||
|
continue
|
||||||
|
elif isinstance(msg, MessageService):
|
||||||
|
# TODO some service messages can be backfilled
|
||||||
|
continue
|
||||||
|
if not before_first_msg_timestamp:
|
||||||
|
first_id = msg.id
|
||||||
|
before_first_msg_timestamp = int(msg.date.timestamp() * 1000) - 1
|
||||||
|
|
||||||
|
converted, intent = await self._convert_batch_msg(source, msg, add_member)
|
||||||
|
events.append(await self._wrap_batch_msg(intent, msg, converted))
|
||||||
|
intents.append(intent)
|
||||||
|
metas.append(msg)
|
||||||
|
if converted.caption:
|
||||||
|
events.append(await self._wrap_batch_msg(intent, msg, converted, caption=True))
|
||||||
|
intents.append(intent)
|
||||||
|
metas.append(None)
|
||||||
|
if len(events) == 0:
|
||||||
|
self.log.debug(
|
||||||
|
f"Didn't get any events to send out of {message_count} messages fetched "
|
||||||
|
f"(first received ID: {first_id})"
|
||||||
|
)
|
||||||
|
return None, message_count, first_id
|
||||||
|
self.log.debug(
|
||||||
|
f"Got {len(events)} events to send out of {message_count} messages fetched "
|
||||||
|
f"(first received ID: {first_id})"
|
||||||
|
)
|
||||||
|
if do_batch_send:
|
||||||
|
resp = await self.main_intent.batch_send(
|
||||||
|
self.mxid,
|
||||||
|
prev_event_id,
|
||||||
|
batch_id=self.next_batch_id if not forward else None,
|
||||||
|
# We iterated the events in reverse chronological order,
|
||||||
|
# so reverse them before sending
|
||||||
|
events=list(reversed(events)),
|
||||||
|
state_events_at_start=state_events,
|
||||||
|
beeper_new_messages=forward,
|
||||||
|
)
|
||||||
|
if prev_event_id == self.first_event_id and resp.next_batch_id:
|
||||||
|
self.next_batch_id = resp.next_batch_id
|
||||||
|
base_insertion_event_id = resp.base_insertion_event_id
|
||||||
|
event_ids = resp.event_ids
|
||||||
|
else:
|
||||||
|
base_insertion_event_id = None
|
||||||
|
event_ids = [
|
||||||
|
await intent.send_message_event(
|
||||||
|
self.mxid, evt.type, evt.content, timestamp=evt.timestamp
|
||||||
|
)
|
||||||
|
for evt, intent in zip(reversed(events), reversed(intents))
|
||||||
|
]
|
||||||
|
tg_space = source.tgid if self.peer_type != "channel" else self.tgid
|
||||||
|
await DBMessage.bulk_insert(
|
||||||
|
[
|
||||||
|
DBMessage(
|
||||||
|
mxid=event_id,
|
||||||
|
mx_room=self.mxid,
|
||||||
|
tgid=msg.id,
|
||||||
|
tg_space=tg_space,
|
||||||
|
edit_index=0,
|
||||||
|
content_hash=self.dedup.hash_event(msg),
|
||||||
|
# TODO sender
|
||||||
|
)
|
||||||
|
# Original arrays are in reverse chronological order, but event IDs are
|
||||||
|
# chronological (because we reversed the original messages list before sending)
|
||||||
|
for event_id, msg in zip(event_ids, reversed(metas))
|
||||||
|
if msg is not None
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return base_insertion_event_id, message_count, first_id
|
||||||
|
|
||||||
def _split_dm_reaction_counts(self, counts: list[ReactionCount]) -> list[MessagePeerReaction]:
|
def _split_dm_reaction_counts(self, counts: list[ReactionCount]) -> list[MessagePeerReaction]:
|
||||||
reactions = []
|
reactions = []
|
||||||
|
|||||||
@@ -399,7 +399,7 @@ class Puppet(DBPuppet, BasePuppet):
|
|||||||
|
|
||||||
async def default_puppet_should_leave_room(self, room_id: RoomID) -> bool:
|
async def default_puppet_should_leave_room(self, room_id: RoomID) -> bool:
|
||||||
portal: p.Portal = await p.Portal.get_by_mxid(room_id)
|
portal: p.Portal = await p.Portal.get_by_mxid(room_id)
|
||||||
return portal and not portal.backfill_lock.locked and portal.peer_type != "user"
|
return portal and portal.peer_type != "user"
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
# region Getters
|
# region Getters
|
||||||
|
|||||||
+109
-5
@@ -20,7 +20,12 @@ from datetime import datetime, timezone
|
|||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from telethon.errors import AuthKeyDuplicatedError, RPCError, UnauthorizedError
|
from telethon.errors import (
|
||||||
|
AuthKeyDuplicatedError,
|
||||||
|
RPCError,
|
||||||
|
TakeoutInitDelayError,
|
||||||
|
UnauthorizedError,
|
||||||
|
)
|
||||||
from telethon.tl.custom import Dialog
|
from telethon.tl.custom import Dialog
|
||||||
from telethon.tl.functions.account import UpdateStatusRequest
|
from telethon.tl.functions.account import UpdateStatusRequest
|
||||||
from telethon.tl.functions.contacts import GetContactsRequest, SearchRequest
|
from telethon.tl.functions.contacts import GetContactsRequest, SearchRequest
|
||||||
@@ -57,7 +62,8 @@ from mautrix.util.opt_prometheus import Gauge
|
|||||||
|
|
||||||
from . import portal as po, puppet as pu, util
|
from . import portal as po, puppet as pu, util
|
||||||
from .abstract_user import AbstractUser
|
from .abstract_user import AbstractUser
|
||||||
from .db import Message as DBMessage, PgSession, User as DBUser
|
from .db import Backfill, Message as DBMessage, PgSession, User as DBUser
|
||||||
|
from .tgclient import MautrixTelegramClient
|
||||||
from .types import TelegramID
|
from .types import TelegramID
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -86,7 +92,11 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
|
|
||||||
_ensure_started_lock: asyncio.Lock
|
_ensure_started_lock: asyncio.Lock
|
||||||
_track_connection_task: asyncio.Task | None
|
_track_connection_task: asyncio.Task | None
|
||||||
|
_backfill_task: asyncio.Task | None
|
||||||
|
wakeup_backfill_task: asyncio.Event
|
||||||
_is_backfilling: bool
|
_is_backfilling: bool
|
||||||
|
takeout_retry_immediate: asyncio.Event
|
||||||
|
takeout_requested: bool
|
||||||
|
|
||||||
_available_emoji_reactions: set[str] | None
|
_available_emoji_reactions: set[str] | None
|
||||||
_available_emoji_reactions_hash: int | None
|
_available_emoji_reactions_hash: int | None
|
||||||
@@ -119,6 +129,12 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
self._track_connection_task = None
|
self._track_connection_task = None
|
||||||
self._is_backfilling = False
|
self._is_backfilling = False
|
||||||
self._portals_cache = None
|
self._portals_cache = None
|
||||||
|
|
||||||
|
self._backfill_task = None
|
||||||
|
self.wakeup_backfill_task = asyncio.Event()
|
||||||
|
self.takeout_retry_immediate = asyncio.Event()
|
||||||
|
self.takeout_requested = False
|
||||||
|
|
||||||
self._available_emoji_reactions = None
|
self._available_emoji_reactions = None
|
||||||
self._available_emoji_reactions_hash = None
|
self._available_emoji_reactions_hash = None
|
||||||
self._available_emoji_reactions_fetched = 0
|
self._available_emoji_reactions_fetched = 0
|
||||||
@@ -248,6 +264,14 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
self.client and self.client._sender and self.client._sender._transport_connected()
|
self.client and self.client._sender and self.client._sender._transport_connected()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _bridge_state_info(self) -> dict[str, Any]:
|
||||||
|
if self.takeout_requested:
|
||||||
|
return {
|
||||||
|
"takeout_requested": True,
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
|
||||||
async def _track_connection(self) -> None:
|
async def _track_connection(self) -> None:
|
||||||
self.log.debug("Starting loop to track connection state")
|
self.log.debug("Starting loop to track connection state")
|
||||||
while True:
|
while True:
|
||||||
@@ -260,6 +284,7 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
if self._is_backfilling
|
if self._is_backfilling
|
||||||
else BridgeStateEvent.CONNECTED,
|
else BridgeStateEvent.CONNECTED,
|
||||||
ttl=3600,
|
ttl=3600,
|
||||||
|
info=self._bridge_state_info,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await self.push_bridge_state(
|
await self.push_bridge_state(
|
||||||
@@ -284,7 +309,7 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
else:
|
else:
|
||||||
state_event = BridgeStateEvent.UNKNOWN_ERROR
|
state_event = BridgeStateEvent.UNKNOWN_ERROR
|
||||||
ttl = 240
|
ttl = 240
|
||||||
return [BridgeState(state_event=state_event, ttl=ttl)]
|
return [BridgeState(state_event=state_event, ttl=ttl, info=self._bridge_state_info)]
|
||||||
|
|
||||||
async def get_puppet(self) -> pu.Puppet | None:
|
async def get_puppet(self) -> pu.Puppet | None:
|
||||||
if not self.tgid:
|
if not self.tgid:
|
||||||
@@ -302,6 +327,9 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
if self._track_connection_task:
|
if self._track_connection_task:
|
||||||
self._track_connection_task.cancel()
|
self._track_connection_task.cancel()
|
||||||
self._track_connection_task = None
|
self._track_connection_task = None
|
||||||
|
if self._backfill_task:
|
||||||
|
self._backfill_task.cancel()
|
||||||
|
self._backfill_task = None
|
||||||
await super().stop()
|
await super().stop()
|
||||||
self._track_metric(METRIC_CONNECTED, False)
|
self._track_metric(METRIC_CONNECTED, False)
|
||||||
|
|
||||||
@@ -318,6 +346,8 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self._track_metric(METRIC_LOGGED_IN, True)
|
self._track_metric(METRIC_LOGGED_IN, True)
|
||||||
|
if not self._backfill_task or self._backfill_task.done():
|
||||||
|
self._backfill_task = asyncio.create_task(self._handle_backfill_requests_loop())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
puppet = await pu.Puppet.get_by_tgid(self.tgid)
|
puppet = await pu.Puppet.get_by_tgid(self.tgid)
|
||||||
@@ -327,7 +357,7 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
except Exception:
|
except Exception:
|
||||||
self.log.exception("Failed to automatically enable custom puppet")
|
self.log.exception("Failed to automatically enable custom puppet")
|
||||||
|
|
||||||
if not self.is_bot and self.config["bridge.startup_sync"]:
|
if not self.is_bot and (self.config["bridge.startup_sync"] or first_login):
|
||||||
try:
|
try:
|
||||||
self._is_backfilling = True
|
self._is_backfilling = True
|
||||||
await self.sync_dialogs()
|
await self.sync_dialogs()
|
||||||
@@ -337,6 +367,80 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
finally:
|
finally:
|
||||||
self._is_backfilling = False
|
self._is_backfilling = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _takeout_options(self) -> dict[str, bool | int]:
|
||||||
|
return {
|
||||||
|
"users": True,
|
||||||
|
"chats": self.config["bridge.backfill.normal_groups"],
|
||||||
|
"megagroups": True,
|
||||||
|
"channels": True,
|
||||||
|
"files": True,
|
||||||
|
"max_file_size": min(self.bridge.matrix.media_config.upload_size, 2000 * 1024 * 1024),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _handle_backfill_requests_loop(self) -> None:
|
||||||
|
while True:
|
||||||
|
req = await Backfill.get_next(self.mxid)
|
||||||
|
if not req:
|
||||||
|
await self.wakeup_backfill_task.wait()
|
||||||
|
self.wakeup_backfill_task.clear()
|
||||||
|
else:
|
||||||
|
await self._takeout_and_backfill(req)
|
||||||
|
|
||||||
|
async def _takeout_and_backfill(self, first_req: Backfill, first_attempt: bool = True) -> None:
|
||||||
|
self.takeout_retry_immediate.clear()
|
||||||
|
self.takeout_requested = True
|
||||||
|
try:
|
||||||
|
async with self.client.takeout(**self._takeout_options) as takeout_client:
|
||||||
|
self.takeout_requested = False
|
||||||
|
self.log.info("Acquired takeout client successfully")
|
||||||
|
await self._backfill_loop_with_client(takeout_client, first_req)
|
||||||
|
self.log.info("Backfills finished, exiting takeout")
|
||||||
|
except TakeoutInitDelayError as e:
|
||||||
|
if first_attempt:
|
||||||
|
self.log.info(
|
||||||
|
f"Takeout requested, will wait for retry request or {e.seconds} seconds"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.log.warning(
|
||||||
|
f"Got takeout init delay again after retry, waiting for {e.seconds} seconds"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(self.takeout_retry_immediate.wait(), timeout=e.seconds)
|
||||||
|
self.log.info("Retrying takeout")
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
self.log.info("Takeout timeout expired")
|
||||||
|
await self._takeout_and_backfill(first_req, first_attempt=False)
|
||||||
|
|
||||||
|
async def _backfill_loop_with_client(
|
||||||
|
self, client: MautrixTelegramClient, first_req: Backfill
|
||||||
|
) -> None:
|
||||||
|
missed_reqs = 0
|
||||||
|
while missed_reqs < 20:
|
||||||
|
req = first_req or await Backfill.get_next(self.mxid)
|
||||||
|
first_req = None
|
||||||
|
if not req:
|
||||||
|
missed_reqs += 1
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(self.wakeup_backfill_task.wait(), timeout=30)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
pass
|
||||||
|
self.wakeup_backfill_task.clear()
|
||||||
|
continue
|
||||||
|
missed_reqs = 0
|
||||||
|
self.log.info("Backfill request %s", req)
|
||||||
|
try:
|
||||||
|
portal = await po.Portal.get_by_tgid(
|
||||||
|
TelegramID(req.portal_tgid), tg_receiver=TelegramID(req.portal_tg_receiver)
|
||||||
|
)
|
||||||
|
await req.mark_dispatched()
|
||||||
|
await portal.backfill(self, client, req=req)
|
||||||
|
await req.mark_done()
|
||||||
|
await asyncio.sleep(req.post_batch_delay)
|
||||||
|
except Exception:
|
||||||
|
self.log.exception("Error handling backfill request for %s", req.portal_tgid)
|
||||||
|
await req.set_cooldown_timeout(10)
|
||||||
|
|
||||||
async def update(self, update: TypeUpdate) -> bool:
|
async def update(self, update: TypeUpdate) -> bool:
|
||||||
if not self.is_bot:
|
if not self.is_bot:
|
||||||
return False
|
return False
|
||||||
@@ -573,7 +677,7 @@ class User(DBUser, AbstractUser, BaseUser):
|
|||||||
was_created = False
|
was_created = False
|
||||||
if portal.mxid:
|
if portal.mxid:
|
||||||
try:
|
try:
|
||||||
await portal.backfill(self, last_id=dialog.message.id)
|
await portal.forward_backfill(self, initial=False, last_tgid=dialog.message.id)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.log.exception(f"Error while backfilling {portal.tgid_log}")
|
self.log.exception(f"Error while backfilling {portal.tgid_log}")
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ class ProvisioningAPI(AuthAPI):
|
|||||||
)
|
)
|
||||||
self.app.router.add_route("POST", f"{user_prefix}/pm/{{identifier}}", self.start_dm)
|
self.app.router.add_route("POST", f"{user_prefix}/pm/{{identifier}}", self.start_dm)
|
||||||
|
|
||||||
|
self.app.router.add_route("POST", f"{user_prefix}/retry_takeout", self.retry_takeout)
|
||||||
|
|
||||||
self.app.router.add_route("POST", f"{user_prefix}/logout", self.logout)
|
self.app.router.add_route("POST", f"{user_prefix}/logout", self.logout)
|
||||||
self.app.router.add_route("POST", f"{user_prefix}/login/bot_token", self.send_bot_token)
|
self.app.router.add_route("POST", f"{user_prefix}/login/bot_token", self.send_bot_token)
|
||||||
self.app.router.add_route("POST", f"{user_prefix}/login/request_code", self.request_code)
|
self.app.router.add_route("POST", f"{user_prefix}/login/request_code", self.request_code)
|
||||||
@@ -494,6 +496,22 @@ class ProvisioningAPI(AuthAPI):
|
|||||||
status=201 if just_created else 200,
|
status=201 if just_created else 200,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def retry_takeout(self, request: web.Request) -> web.Response:
|
||||||
|
data, user, err = await self.get_user_request_info(
|
||||||
|
request, expect_logged_in=True, want_data=False
|
||||||
|
)
|
||||||
|
if err is not None:
|
||||||
|
return err
|
||||||
|
if not user.takeout_requested:
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"error": "There was no takeout requested",
|
||||||
|
},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
user.takeout_retry_immediate.set()
|
||||||
|
return web.json_response({}, status=200)
|
||||||
|
|
||||||
async def send_bot_token(self, request: web.Request) -> web.Response:
|
async def send_bot_token(self, request: web.Request) -> web.Response:
|
||||||
data, user, err = await self.get_user_request_info(request)
|
data, user, err = await self.get_user_request_info(request)
|
||||||
if err is not None:
|
if err is not None:
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ python-magic>=0.4,<0.5
|
|||||||
commonmark>=0.8,<0.10
|
commonmark>=0.8,<0.10
|
||||||
aiohttp>=3,<4
|
aiohttp>=3,<4
|
||||||
yarl>=1,<2
|
yarl>=1,<2
|
||||||
mautrix>=0.18.2,<0.19
|
mautrix>=0.18.3,<0.19
|
||||||
#telethon>=1.24,<1.25
|
#telethon>=1.24,<1.25
|
||||||
tulir-telethon==1.26.0a6
|
tulir-telethon==1.26.0a6
|
||||||
asyncpg>=0.20,<0.27
|
asyncpg>=0.20,<0.27
|
||||||
|
|||||||
Reference in New Issue
Block a user