Compare commits
25 Commits
v0.8.0-rc1
...
v0.8.0-rc4
| Author | SHA1 | Date | |
|---|---|---|---|
| ee04e8c17f | |||
| 7d75c15027 | |||
| 312a44d361 | |||
| 85d38e3db6 | |||
| 3a25ee2c93 | |||
| a4d49a41e0 | |||
| 7ba9e10f0f | |||
| 05e966011e | |||
| 9081f6bce4 | |||
| c126e8b615 | |||
| f454803ef7 | |||
| 40beb8f752 | |||
| 4d8d332732 | |||
| 7fb771b992 | |||
| d0900a95a7 | |||
| 8552d463a1 | |||
| 74d130644c | |||
| 976e0dd2b7 | |||
| 340c25ba0b | |||
| 7e8d4bc9a8 | |||
| 429544373a | |||
| 80dd6fa9e1 | |||
| 45ac120407 | |||
| 2c100ca1e5 | |||
| c54bd9e1ce |
+2
-2
@@ -36,6 +36,6 @@ manifest:
|
||||
script:
|
||||
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64
|
||||
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64
|
||||
- if [ $CI_COMMIT_BRANCH == "master" ]; then docker manifest create $CI_REGISTRY_IMAGE:latest $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 && docker manifest push $CI_REGISTRY_IMAGE:latest; fi
|
||||
- if [ $CI_COMMIT_BRANCH != "master" ]; then docker manifest create $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 && docker manifest push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME; fi
|
||||
- if [ "$CI_COMMIT_BRANCH" = "master" ]; then docker manifest create $CI_REGISTRY_IMAGE:latest $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 && docker manifest push $CI_REGISTRY_IMAGE:latest; fi
|
||||
- if [ "$CI_COMMIT_BRANCH" != "master" ]; then docker manifest create $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64 && docker manifest push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME; fi
|
||||
- docker rmi $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-amd64 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-arm64
|
||||
|
||||
+3
-1
@@ -62,7 +62,9 @@ RUN apk add --virtual .build-deps \
|
||||
&& apk del .build-deps
|
||||
|
||||
COPY . /opt/mautrix-telegram
|
||||
RUN apk add git && pip3 install .[speedups,hq_thumbnails,metrics,e2be] && apk del git
|
||||
RUN apk add git && pip3 install .[speedups,hq_thumbnails,metrics,e2be] && apk del git \
|
||||
# This doesn't make the image smaller, but it's needed so that the `version` command works properly
|
||||
&& cp mautrix_telegram/example-config.yaml . && rm -rf mautrix_telegram
|
||||
|
||||
VOLUME /data
|
||||
ENV UID=1337 GID=1337 \
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
include README.md
|
||||
include LICENSE
|
||||
include requirements.txt
|
||||
include optional-requirements.txt
|
||||
@@ -7,6 +7,9 @@
|
||||
|
||||
A Matrix-Telegram hybrid puppeting/relaybot bridge.
|
||||
|
||||
## Sponsors
|
||||
* [Joel Lehtonen / Zouppen](https://github.com/zouppen)
|
||||
|
||||
### [Wiki](https://github.com/tulir/mautrix-telegram/wiki)
|
||||
|
||||
### [Features & Roadmap](https://github.com/tulir/mautrix-telegram/blob/master/ROADMAP.md)
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ if [ -f /data/mx-state.json ]; then
|
||||
fi
|
||||
|
||||
if [ ! -f /data/config.yaml ]; then
|
||||
cp mautrix_telegram/example-config.yaml /data/config.yaml
|
||||
cp example-config.yaml /data/config.yaml
|
||||
echo "Didn't find a config file."
|
||||
echo "Copied default config file to /data/config.yaml"
|
||||
echo "Modify that config file to your liking."
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
__version__ = "0.8.0rc1"
|
||||
__version__ = "0.8.0rc4"
|
||||
__author__ = "Tulir Asokan <tulir@maunium.net>"
|
||||
|
||||
@@ -35,6 +35,7 @@ from telethon.tl.types import (
|
||||
from mautrix.types import UserID, PresenceState
|
||||
from mautrix.errors import MatrixError
|
||||
from mautrix.appservice import AppService
|
||||
from mautrix.util.logging import TraceLogger
|
||||
from alchemysession import AlchemySessionContainer
|
||||
|
||||
from . import portal as po, puppet as pu, __version__
|
||||
@@ -68,7 +69,7 @@ except ImportError:
|
||||
class AbstractUser(ABC):
|
||||
session_container: AlchemySessionContainer = None
|
||||
loop: asyncio.AbstractEventLoop = None
|
||||
log: logging.Logger
|
||||
log: TraceLogger
|
||||
az: AppService
|
||||
relaybot: Optional['Bot']
|
||||
ignore_incoming_bot_events: bool = True
|
||||
@@ -258,7 +259,7 @@ class AbstractUser(ABC):
|
||||
elif isinstance(update, UpdateReadHistoryOutbox):
|
||||
await self.update_read_receipt(update)
|
||||
else:
|
||||
self.log.debug("Unhandled update: %s", update)
|
||||
self.log.trace("Unhandled update: %s", update)
|
||||
|
||||
async def update_pinned_messages(self, update: Union[UpdateChannelPinnedMessage,
|
||||
UpdateChatPinnedMessage]) -> None:
|
||||
@@ -333,7 +334,7 @@ class AbstractUser(ABC):
|
||||
if await puppet.update_avatar(self, update.photo):
|
||||
puppet.save()
|
||||
else:
|
||||
self.log.warning("Unexpected other user info update: %s", update)
|
||||
self.log.warning(f"Unexpected other user info update: {type(update)}")
|
||||
|
||||
async def update_status(self, update: UpdateUserStatus) -> None:
|
||||
puppet = pu.Puppet.get(TelegramID(update.user_id))
|
||||
@@ -342,7 +343,7 @@ class AbstractUser(ABC):
|
||||
elif isinstance(update.status, UserStatusOffline):
|
||||
await puppet.default_mxid_intent.set_presence(PresenceState.OFFLINE)
|
||||
else:
|
||||
self.log.warning("Unexpected user status update: %s", update)
|
||||
self.log.warning(f"Unexpected user status update: type({update})")
|
||||
return
|
||||
|
||||
def get_message_details(self, update: UpdateMessage) -> Tuple[UpdateMessageContent,
|
||||
@@ -366,8 +367,7 @@ class AbstractUser(ABC):
|
||||
portal = po.Portal.get_by_entity(update.to_id, receiver_id=self.tgid)
|
||||
sender = pu.Puppet.get(update.from_id) if update.from_id else None
|
||||
else:
|
||||
self.log.warning(
|
||||
f"Unexpected message type in User#get_message_details: {type(update)}")
|
||||
self.log.warning(f"Unexpected message type in User#get_message_details: {type(update)}")
|
||||
return update, None, None
|
||||
return update, sender, portal
|
||||
|
||||
@@ -428,11 +428,10 @@ class AbstractUser(ABC):
|
||||
|
||||
if isinstance(update, MessageService):
|
||||
if isinstance(update.action, MessageActionChannelMigrateFrom):
|
||||
self.log.debug(f"Ignoring action %s to %s by %d", update.action,
|
||||
portal.tgid_log,
|
||||
self.log.trace(f"Ignoring action %s to %s by %d", update.action, portal.tgid_log,
|
||||
sender.id)
|
||||
return
|
||||
self.log.debug("Handling action %s to %s by %d", update.action, portal.tgid_log,
|
||||
self.log.trace("Handling action %s to %s by %d", update.action, portal.tgid_log,
|
||||
sender.id)
|
||||
return await portal.handle_telegram_action(self, sender, update)
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ class Bot(AbstractUser):
|
||||
if self.whitelist_group_admins:
|
||||
if isinstance(chat, PeerChannel):
|
||||
p = await self.client(GetParticipantRequest(chat, tgid))
|
||||
return isinstance(p, (ChannelParticipantCreator, ChannelParticipantAdmin))
|
||||
return isinstance(p.participant, (ChannelParticipantCreator, ChannelParticipantAdmin))
|
||||
elif isinstance(chat, PeerChat):
|
||||
chat = await self.client(GetFullChatRequest(chat.chat_id))
|
||||
participants = chat.full_chat.participants.participants
|
||||
|
||||
@@ -22,9 +22,7 @@ from .. import command_handler, CommandEvent, SECTION_PORTAL_MANAGEMENT
|
||||
from .util import user_has_power_level
|
||||
|
||||
|
||||
async def _get_portal_and_check_permission(evt: CommandEvent, permission: str,
|
||||
action: Optional[str] = None
|
||||
) -> Optional[po.Portal]:
|
||||
async def _get_portal_and_check_permission(evt: CommandEvent) -> Optional[po.Portal]:
|
||||
room_id = RoomID(evt.args[0]) if len(evt.args) > 0 else evt.room_id
|
||||
|
||||
portal = po.Portal.get_by_mxid(room_id)
|
||||
@@ -33,9 +31,8 @@ async def _get_portal_and_check_permission(evt: CommandEvent, permission: str,
|
||||
await evt.reply(f"{that_this} is not a portal room.")
|
||||
return None
|
||||
|
||||
if not await user_has_power_level(portal.mxid, evt.az.intent, evt.sender, permission):
|
||||
action = action or f"{permission.replace('_', ' ')}s"
|
||||
await evt.reply(f"You do not have the permissions to {action} that portal.")
|
||||
if not await user_has_power_level(portal.mxid, evt.az.intent, evt.sender, "unbridge"):
|
||||
await evt.reply("You do not have the permissions to unbridge that portal.")
|
||||
return None
|
||||
return portal
|
||||
|
||||
@@ -64,7 +61,7 @@ def _get_portal_murder_function(action: str, room_id: str, function: Callable, c
|
||||
"Only works for group chats; to delete a private chat portal, simply "
|
||||
"leave the room.")
|
||||
async def delete_portal(evt: CommandEvent) -> Optional[EventID]:
|
||||
portal = await _get_portal_and_check_permission(evt, "unbridge")
|
||||
portal = await _get_portal_and_check_permission(evt)
|
||||
if not portal:
|
||||
return None
|
||||
|
||||
@@ -85,7 +82,7 @@ async def delete_portal(evt: CommandEvent) -> Optional[EventID]:
|
||||
help_section=SECTION_PORTAL_MANAGEMENT,
|
||||
help_text="Remove puppets from the current portal room and forget the portal.")
|
||||
async def unbridge(evt: CommandEvent) -> Optional[EventID]:
|
||||
portal = await _get_portal_and_check_permission(evt, "unbridge")
|
||||
portal = await _get_portal_and_check_permission(evt)
|
||||
if not portal:
|
||||
return None
|
||||
|
||||
|
||||
@@ -165,7 +165,9 @@ async def join(evt: CommandEvent) -> Optional[EventID]:
|
||||
try:
|
||||
await portal.create_matrix_room(evt.sender, chat, [evt.sender.mxid])
|
||||
except ChatIdInvalidError as e:
|
||||
logging.getLogger("mau.commands").info(updates.stringify())
|
||||
logging.getLogger("mau.commands").trace("ChatIdInvalidError while creating portal "
|
||||
"from !tg join command: %s",
|
||||
updates.stringify())
|
||||
raise e
|
||||
return await evt.reply(f"Created room for {portal.title}")
|
||||
return None
|
||||
@@ -328,9 +330,11 @@ async def random(evt: CommandEvent) -> EventID:
|
||||
|
||||
|
||||
@command_handler(help_section=SECTION_PORTAL_MANAGEMENT,
|
||||
help_args="<_number of messages_> [--takeout]",
|
||||
help_text="Backfill messages from Telegram history.")
|
||||
async def backfill(evt: CommandEvent) -> None:
|
||||
if not evt.is_portal:
|
||||
await evt.reply("You can only use backfill in portal rooms")
|
||||
return
|
||||
portal = po.Portal.get_by_mxid(evt.room_id)
|
||||
try:
|
||||
await portal.backfill(evt.sender)
|
||||
|
||||
@@ -123,6 +123,7 @@ class Config(BaseBridgeConfig):
|
||||
copy("bridge.animated_sticker.args")
|
||||
copy("bridge.encryption.allow")
|
||||
copy("bridge.encryption.default")
|
||||
copy("bridge.private_chat_portal_meta")
|
||||
|
||||
copy("bridge.initial_power_level_overrides.group")
|
||||
copy("bridge.initial_power_level_overrides.user")
|
||||
|
||||
@@ -207,6 +207,9 @@ bridge:
|
||||
# Default to encryption, force-enable encryption in all portals the bridge creates
|
||||
# This will cause the bridge bot to be in private chats for the encryption to work properly.
|
||||
default: false
|
||||
# Whether or not to explicitly set the avatar and room name for private
|
||||
# chat portal rooms. This will be implicitly enabled if encryption.default is true.
|
||||
private_chat_portal_meta: false
|
||||
|
||||
# Overrides for base power levels.
|
||||
initial_power_level_overrides:
|
||||
|
||||
@@ -32,6 +32,7 @@ from mautrix.errors import MatrixRequestError, IntentError
|
||||
from mautrix.appservice import AppService, IntentAPI
|
||||
from mautrix.types import RoomID, RoomAlias, UserID, EventType, PowerLevelStateEventContent
|
||||
from mautrix.util.simple_template import SimpleTemplate
|
||||
from mautrix.util.logging import TraceLogger
|
||||
|
||||
from ..types import TelegramID
|
||||
from ..context import Context
|
||||
@@ -55,7 +56,7 @@ config: Optional['Config'] = None
|
||||
|
||||
|
||||
class BasePortal(ABC):
|
||||
base_log: logging.Logger = logging.getLogger("mau.portal")
|
||||
base_log: TraceLogger = logging.getLogger("mau.portal")
|
||||
az: AppService = None
|
||||
bot: 'Bot' = None
|
||||
loop: asyncio.AbstractEventLoop = None
|
||||
@@ -69,6 +70,7 @@ class BasePortal(ABC):
|
||||
sync_channel_members: bool = True
|
||||
sync_matrix_state: bool = True
|
||||
public_portals: bool = False
|
||||
private_chat_portal_meta: bool = False
|
||||
|
||||
alias_template: SimpleTemplate[str]
|
||||
hs_domain: str
|
||||
@@ -91,7 +93,7 @@ class BasePortal(ABC):
|
||||
deleted: bool
|
||||
backfilling: bool
|
||||
backfill_leave: Optional[Set[IntentAPI]]
|
||||
log: logging.Logger
|
||||
log: TraceLogger
|
||||
|
||||
alias: Optional[RoomAlias]
|
||||
|
||||
@@ -242,8 +244,7 @@ class BasePortal(ABC):
|
||||
return await user.client.get_entity(self.peer)
|
||||
except ValueError:
|
||||
if user.is_bot:
|
||||
self.log.warning(f"Could not find entity with bot {user.tgid}. "
|
||||
"Failing...")
|
||||
self.log.warning(f"Could not find entity with bot {user.tgid}. Failing...")
|
||||
raise
|
||||
self.log.warning(f"Could not find entity with user {user.tgid}. "
|
||||
"falling back to get_dialogs.")
|
||||
@@ -402,6 +403,8 @@ class BasePortal(ABC):
|
||||
@classmethod
|
||||
def get_by_tgid(cls, tgid: TelegramID, tg_receiver: Optional[TelegramID] = None,
|
||||
peer_type: str = None) -> Optional['Portal']:
|
||||
if peer_type == "user" and tg_receiver is None:
|
||||
raise ValueError("tg_receiver is required when peer_type is \"user\"")
|
||||
tg_receiver = tg_receiver or tgid
|
||||
tgid_full = (tgid, tg_receiver)
|
||||
try:
|
||||
@@ -516,6 +519,7 @@ def init(context: Context) -> None:
|
||||
BasePortal.sync_channel_members = config["bridge.sync_channel_members"]
|
||||
BasePortal.sync_matrix_state = config["bridge.sync_matrix_state"]
|
||||
BasePortal.public_portals = config["bridge.public_portals"]
|
||||
BasePortal.private_chat_portal_meta = config["bridge.private_chat_portal_meta"]
|
||||
BasePortal.filter_mode = config["bridge.filter.mode"]
|
||||
BasePortal.filter_list = config["bridge.filter.list"]
|
||||
BasePortal.hs_domain = config["homeserver.domain"]
|
||||
|
||||
@@ -342,7 +342,7 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
|
||||
|
||||
def _add_telegram_message_to_db(self, event_id: EventID, space: TelegramID,
|
||||
edit_index: int, response: TypeMessage) -> None:
|
||||
self.log.debug("Handled Matrix message: %s", response)
|
||||
self.log.trace("Handled Matrix message: %s", response)
|
||||
self.dedup.check(response, (event_id, space), force_hash=edit_index != 0)
|
||||
if edit_index < 0:
|
||||
prev_edit = DBMessage.get_one_by_tgid(TelegramID(response.id), space, -1)
|
||||
@@ -403,7 +403,7 @@ class PortalMatrix(BasePortal, MautrixBasePortal, ABC):
|
||||
await self._handle_matrix_file(sender_id, event_id, space, client, content, reply_to,
|
||||
caption_content)
|
||||
else:
|
||||
self.log.debug(f"Unhandled Matrix event: {content}")
|
||||
self.log.trace("Unhandled Matrix event: %s", content)
|
||||
|
||||
async def handle_matrix_pin(self, sender: 'u.User',
|
||||
pinned_message: Optional[EventID]) -> None:
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#
|
||||
# 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 typing import List, Optional, Tuple, Union, Callable, TYPE_CHECKING
|
||||
from typing import List, Optional, Tuple, Union, Callable, Awaitable, TYPE_CHECKING
|
||||
from abc import ABC
|
||||
import asyncio
|
||||
|
||||
@@ -26,7 +26,7 @@ from telethon.tl.types import (
|
||||
Channel, ChatBannedRights, ChannelParticipantsRecent, ChannelParticipantsSearch, ChatPhoto,
|
||||
PhotoEmpty, InputChannel, InputUser, ChatPhotoEmpty, PeerUser, Photo, TypeChat, TypeInputPeer,
|
||||
TypeUser, User, InputPeerPhotoFileLocation, ChatParticipantAdmin, ChannelParticipantAdmin,
|
||||
ChatParticipantCreator, ChannelParticipantCreator)
|
||||
ChatParticipantCreator, ChannelParticipantCreator, UserProfilePhoto, UserProfilePhotoEmpty)
|
||||
|
||||
from mautrix.errors import MForbidden
|
||||
from mautrix.types import (RoomID, UserID, RoomCreatePreset, EventType, Membership, Member,
|
||||
@@ -218,10 +218,17 @@ class PortalMetadata(BasePortal, ABC):
|
||||
puppet = p.Puppet.get(self.tgid)
|
||||
await puppet.update_info(user, entity)
|
||||
await puppet.intent_for(self).join_room(self.mxid)
|
||||
if self.encrypted or self.private_chat_portal_meta:
|
||||
# The bridge bot needs to join for e2ee, but that messes up the default name
|
||||
# generation. If/when canonical DMs happen, this might not be necessary anymore.
|
||||
changed = await self._update_title(puppet.displayname)
|
||||
changed = await self._update_avatar(user, entity.photo) or changed
|
||||
if changed:
|
||||
self.save()
|
||||
if self.sync_matrix_state:
|
||||
await self.sync_matrix_members()
|
||||
|
||||
async def create_matrix_room(self, user: 'AbstractUser', entity: TypeChat = None,
|
||||
async def create_matrix_room(self, user: 'AbstractUser', entity: Union[TypeChat, User] = None,
|
||||
invites: InviteList = None, update_if_exists: bool = True,
|
||||
synchronous: bool = False) -> Optional[str]:
|
||||
if self.mxid:
|
||||
@@ -245,8 +252,8 @@ class PortalMetadata(BasePortal, ABC):
|
||||
except Exception:
|
||||
self.log.exception("Fatal error creating Matrix room")
|
||||
|
||||
async def _create_matrix_room(self, user: 'AbstractUser', entity: TypeChat, invites: InviteList
|
||||
) -> Optional[RoomID]:
|
||||
async def _create_matrix_room(self, user: 'AbstractUser', entity: Union[TypeChat, User],
|
||||
invites: InviteList) -> Optional[RoomID]:
|
||||
direct = self.peer_type == "user"
|
||||
|
||||
if invites is None:
|
||||
@@ -260,7 +267,7 @@ class PortalMetadata(BasePortal, ABC):
|
||||
|
||||
if not entity:
|
||||
entity = await self.get_entity(user)
|
||||
self.log.debug(f"Fetched data: {entity}")
|
||||
self.log.trace("Fetched data: %s", entity)
|
||||
|
||||
self.log.debug("Creating room")
|
||||
|
||||
@@ -274,6 +281,8 @@ class PortalMetadata(BasePortal, ABC):
|
||||
self.about = "Your Telegram cloud storage chat"
|
||||
|
||||
puppet = p.Puppet.get(self.tgid) if direct else None
|
||||
if puppet:
|
||||
await puppet.update_info(user, entity)
|
||||
self._main_intent = puppet.intent_for(self) if direct else self.az.intent
|
||||
|
||||
if self.peer_type == "channel":
|
||||
@@ -307,9 +316,30 @@ class PortalMetadata(BasePortal, ABC):
|
||||
for invite in invites:
|
||||
power_levels.users.setdefault(invite, 100)
|
||||
self.title = puppet.displayname
|
||||
bridge_info = {
|
||||
"bridgebot": self.az.bot_mxid,
|
||||
"creator": self.main_intent.mxid,
|
||||
"protocol": {
|
||||
"id": "telegram",
|
||||
"displayname": "Telegram",
|
||||
"avatar_url": config["appservice.bot_avatar"],
|
||||
},
|
||||
"channel": {
|
||||
"id": self.tgid
|
||||
}
|
||||
}
|
||||
initial_state = [{
|
||||
"type": EventType.ROOM_POWER_LEVELS.serialize(),
|
||||
"content": power_levels.serialize(),
|
||||
}, {
|
||||
"type": "m.bridge",
|
||||
"state_key": f"net.maunium.telegram://telegram/{self.tgid}",
|
||||
"content": bridge_info
|
||||
}, {
|
||||
# TODO remove this once https://github.com/matrix-org/matrix-doc/pull/2346 is in spec
|
||||
"type": "uk.half-shot.bridge",
|
||||
"state_key": f"net.maunium.telegram://telegram/{self.tgid}",
|
||||
"content": bridge_info
|
||||
}]
|
||||
if config["bridge.encryption.default"] and self.matrix.e2ee:
|
||||
self.encrypted = True
|
||||
@@ -319,9 +349,8 @@ class PortalMetadata(BasePortal, ABC):
|
||||
})
|
||||
if direct:
|
||||
invites.append(self.az.bot_mxid)
|
||||
# The bridge bot needs to join for e2ee, but that messes up the default name
|
||||
# generation. If/when canonical DMs happen, this might not be necessary anymore.
|
||||
self.title = puppet.displayname
|
||||
if direct and (self.encrypted or self.private_chat_portal_meta):
|
||||
self.title = puppet.displayname
|
||||
if config["appservice.community_id"]:
|
||||
initial_state.append({
|
||||
"type": "m.room.related_groups",
|
||||
@@ -566,12 +595,12 @@ class PortalMetadata(BasePortal, ABC):
|
||||
self.log.warning("Called update_info() for direct chat portal")
|
||||
return
|
||||
|
||||
changed = False
|
||||
self.log.debug("Updating info")
|
||||
try:
|
||||
if not entity:
|
||||
entity = await self.get_entity(user)
|
||||
self.log.debug(f"Fetched data: {entity}")
|
||||
changed = False
|
||||
self.log.trace("Fetched data: %s", entity)
|
||||
|
||||
if self.peer_type == "channel":
|
||||
changed = self.megagroup != entity.megagroup or changed
|
||||
@@ -610,7 +639,7 @@ class PortalMetadata(BasePortal, ABC):
|
||||
return True
|
||||
|
||||
async def _try_use_intent(self, sender: Optional['p.Puppet'],
|
||||
action: Callable[[IntentAPI], None]) -> None:
|
||||
action: Callable[[IntentAPI], Awaitable[None]]) -> None:
|
||||
if sender:
|
||||
try:
|
||||
await action(sender.intent_for(self))
|
||||
@@ -645,18 +674,19 @@ class PortalMetadata(BasePortal, ABC):
|
||||
|
||||
async def _update_avatar(self, user: 'AbstractUser', photo: TypeChatPhoto,
|
||||
sender: Optional['p.Puppet'] = None, save: bool = False) -> bool:
|
||||
if isinstance(photo, ChatPhoto):
|
||||
if isinstance(photo, (ChatPhoto, UserProfilePhoto)):
|
||||
loc = InputPeerPhotoFileLocation(
|
||||
peer=await self.get_input_entity(user),
|
||||
local_id=photo.photo_big.local_id,
|
||||
volume_id=photo.photo_big.volume_id,
|
||||
big=True
|
||||
)
|
||||
photo_id = f"{loc.volume_id}-{loc.local_id}"
|
||||
photo_id = (f"{loc.volume_id}-{loc.local_id}" if isinstance(photo, ChatPhoto)
|
||||
else photo.photo_id)
|
||||
elif isinstance(photo, Photo):
|
||||
loc, largest = self._get_largest_photo_size(photo)
|
||||
photo_id = f"{largest.location.volume_id}-{largest.location.local_id}"
|
||||
elif isinstance(photo, (ChatPhotoEmpty, PhotoEmpty)):
|
||||
elif isinstance(photo, (UserProfilePhotoEmpty, ChatPhotoEmpty, PhotoEmpty, type(None))):
|
||||
photo_id = ""
|
||||
loc = None
|
||||
else:
|
||||
|
||||
@@ -80,7 +80,7 @@ class PortalTelegram(BasePortal, ABC):
|
||||
return await intent.send_message_event(self.mxid, event_type, content, **kwargs)
|
||||
|
||||
async def handle_telegram_photo(self, source: 'AbstractUser', intent: IntentAPI, evt: Message,
|
||||
relates_to: Dict = None) -> Optional[EventID]:
|
||||
relates_to: RelatesTo = None) -> Optional[EventID]:
|
||||
loc, largest_size = self._get_largest_photo_size(evt.media.photo)
|
||||
file = await util.transfer_file_to_matrix(source.client, intent, loc,
|
||||
encrypt=self.encrypted)
|
||||
@@ -227,8 +227,8 @@ class PortalTelegram(BasePortal, ABC):
|
||||
content.url = file.mxc
|
||||
return await self._send_message(intent, content, event_type=event_type, timestamp=evt.date)
|
||||
|
||||
def handle_telegram_location(self, _: 'AbstractUser', intent: IntentAPI, evt: Message,
|
||||
relates_to: dict = None) -> Awaitable[EventID]:
|
||||
def handle_telegram_location(self, source: 'AbstractUser', intent: IntentAPI, evt: Message,
|
||||
relates_to: RelatesTo = None) -> Awaitable[EventID]:
|
||||
long = evt.media.geo.long
|
||||
lat = evt.media.geo.lat
|
||||
long_char = "E" if long > 0 else "W"
|
||||
@@ -257,7 +257,7 @@ class PortalTelegram(BasePortal, ABC):
|
||||
return await self._send_message(intent, content, timestamp=evt.date)
|
||||
|
||||
async def handle_telegram_unsupported(self, source: 'AbstractUser', intent: IntentAPI,
|
||||
evt: Message, relates_to: dict = None) -> EventID:
|
||||
evt: Message, relates_to: RelatesTo = None) -> EventID:
|
||||
override_text = ("This message is not supported on your version of Mautrix-Telegram. "
|
||||
"Please check https://github.com/tulir/mautrix-telegram or ask your "
|
||||
"bridge administrator about possible updates.")
|
||||
@@ -312,7 +312,7 @@ class PortalTelegram(BasePortal, ABC):
|
||||
|
||||
@staticmethod
|
||||
def _int_to_bytes(i: int) -> bytes:
|
||||
hex_value = "{0:010x}".format(i)
|
||||
hex_value = "{0:010x}".format(i).encode("utf-8")
|
||||
return codecs.decode(hex_value, "hex_codec")
|
||||
|
||||
def _encode_msgid(self, source: 'AbstractUser', evt: Message) -> str:
|
||||
@@ -355,6 +355,7 @@ class PortalTelegram(BasePortal, ABC):
|
||||
async def handle_telegram_edit(self, source: 'AbstractUser', sender: p.Puppet, evt: Message
|
||||
) -> None:
|
||||
if not self.mxid:
|
||||
self.log.trace("Ignoring edit to %d as chat has no Matrix room", evt.id)
|
||||
return
|
||||
elif hasattr(evt, "media") and isinstance(evt.media, MessageMediaGame):
|
||||
self.log.debug("Ignoring game message edit event")
|
||||
@@ -402,17 +403,21 @@ class PortalTelegram(BasePortal, ABC):
|
||||
DBMessage.update_by_mxid(temporary_identifier, self.mxid, mxid=event_id)
|
||||
|
||||
async def backfill(self, source: 'AbstractUser') -> None:
|
||||
self.log.debug("Backfilling history through %s", source.mxid)
|
||||
last = DBMessage.find_last(self.mxid, (source.tgid if self.peer_type != "channel"
|
||||
else self.tgid))
|
||||
min_id = last.tgid if last else 0
|
||||
self.backfilling = True
|
||||
self.backfill_leave = set()
|
||||
if self.peer_type == "user":
|
||||
self.log.debug("Adding %s's default puppet to room for backfilling", source.mxid)
|
||||
sender = p.Puppet.get(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)
|
||||
max_file_size = min(config["bridge.max_document_size"], 1500) * 1024 * 1024
|
||||
self.log.trace("Opening takeout client for %d, message ID %d->", source.tgid, min_id)
|
||||
count = 0
|
||||
async with source.client.takeout(files=True, megagroups=self.megagroup,
|
||||
chats=self.peer_type == "chat",
|
||||
users=self.peer_type == "user",
|
||||
@@ -426,14 +431,18 @@ class PortalTelegram(BasePortal, ABC):
|
||||
# if isinstance(message, MessageService):
|
||||
# await self.handle_telegram_action(source, sender, message)
|
||||
await self.handle_telegram_message(source, sender, message)
|
||||
count += 1
|
||||
for intent in self.backfill_leave:
|
||||
self.log.trace("Leaving room with %s post-backfill", intent.mxid)
|
||||
await intent.leave_room(self.mxid)
|
||||
self.backfilling = False
|
||||
self.backfill_leave = None
|
||||
self.log.info("Backfilled %d messages through %s", count, source.mxid)
|
||||
|
||||
async def handle_telegram_message(self, source: 'AbstractUser', sender: p.Puppet,
|
||||
evt: Message) -> None:
|
||||
if not self.mxid:
|
||||
self.log.trace("Got telegram message %d, but no room exists, creating...", evt.id)
|
||||
await self.create_matrix_room(source, invites=[source.mxid], update_if_exists=False)
|
||||
|
||||
if (self.peer_type == "user" and sender.tgid == self.tg_receiver
|
||||
@@ -467,6 +476,8 @@ class PortalTelegram(BasePortal, ABC):
|
||||
"bridge.deduplication.cache_queue_length in the config.")
|
||||
return
|
||||
|
||||
self.log.trace("Handling Telegram message %s", evt)
|
||||
|
||||
if sender and not sender.displayname:
|
||||
self.log.debug(f"Telegram user {sender.tgid} sent a message, but doesn't have a "
|
||||
"displayname, updating info...")
|
||||
@@ -500,7 +511,7 @@ class PortalTelegram(BasePortal, ABC):
|
||||
}[type(media)](source, intent, evt,
|
||||
relates_to=formatter.telegram_reply_to_matrix(evt, source))
|
||||
else:
|
||||
self.log.debug("Unhandled Telegram message: %s", evt)
|
||||
self.log.debug("Unhandled Telegram message %d", evt.id)
|
||||
return
|
||||
|
||||
if not event_id:
|
||||
@@ -517,7 +528,7 @@ class PortalTelegram(BasePortal, ABC):
|
||||
await intent.redact(self.mxid, event_id)
|
||||
return
|
||||
|
||||
self.log.debug("Handled Telegram message: %s", evt)
|
||||
self.log.debug("Handled telegram message %d -> %s", evt.id, event_id)
|
||||
try:
|
||||
DBMessage(tgid=TelegramID(evt.id), mx_room=self.mxid, mxid=event_id,
|
||||
tg_space=tg_space, edit_index=0).insert()
|
||||
@@ -572,7 +583,7 @@ class PortalTelegram(BasePortal, ABC):
|
||||
# TODO handle game score
|
||||
pass
|
||||
else:
|
||||
self.log.debug("Unhandled Telegram action in %s: %s", self.title, action)
|
||||
self.log.trace("Unhandled Telegram action in %s: %s", self.title, action)
|
||||
|
||||
async def set_telegram_admin(self, user_id: TelegramID) -> None:
|
||||
puppet = p.Puppet.get(user_id)
|
||||
|
||||
+19
-12
@@ -242,8 +242,7 @@ class Puppet(CustomPuppetMixin):
|
||||
|
||||
try:
|
||||
changed = await self.update_displayname(source, info) or changed
|
||||
if isinstance(info.photo, UserProfilePhoto):
|
||||
changed = await self.update_avatar(source, info.photo) or changed
|
||||
changed = await self.update_avatar(source, info.photo) or changed
|
||||
except Exception:
|
||||
self.log.exception(f"Failed to update info from source {source.tgid}")
|
||||
|
||||
@@ -256,19 +255,24 @@ class Puppet(CustomPuppetMixin):
|
||||
) -> bool:
|
||||
if self.disable_updates:
|
||||
return False
|
||||
allow_source = (source.is_relaybot
|
||||
or self.displayname_source == source.tgid
|
||||
# User is not a contact, so there's no custom name
|
||||
or not info.contact
|
||||
# No displayname source, so just trust anything
|
||||
or self.displayname_source is None)
|
||||
if not allow_source:
|
||||
if source.is_relaybot or source.is_bot:
|
||||
allow_because = "user is bot"
|
||||
elif self.displayname_source == source.tgid:
|
||||
allow_because = "user is the primary source"
|
||||
elif not info.contact:
|
||||
allow_because = "user is not a contact"
|
||||
elif self.displayname_source is None:
|
||||
allow_because = "no primary source set"
|
||||
else:
|
||||
return False
|
||||
elif isinstance(info, UpdateUserName):
|
||||
|
||||
if isinstance(info, UpdateUserName):
|
||||
info = await source.client.get_entity(PeerUser(self.tgid))
|
||||
|
||||
displayname = self.get_displayname(info)
|
||||
if displayname != self.displayname:
|
||||
self.log.debug(f"Updating displayname of {self.id} (src: {source.tgid}, allowed "
|
||||
f"because {allow_because}) from {self.displayname} to {displayname}")
|
||||
self.displayname = displayname
|
||||
self.displayname_source = source.tgid
|
||||
try:
|
||||
@@ -289,10 +293,13 @@ class Puppet(CustomPuppetMixin):
|
||||
if self.disable_updates:
|
||||
return False
|
||||
|
||||
if isinstance(photo, UserProfilePhotoEmpty):
|
||||
if photo is None or isinstance(photo, UserProfilePhotoEmpty):
|
||||
photo_id = ""
|
||||
else:
|
||||
elif isinstance(photo, UserProfilePhoto):
|
||||
photo_id = str(photo.photo_id)
|
||||
else:
|
||||
self.log.warning(f"Unknown user profile photo type: {type(photo)}")
|
||||
return False
|
||||
if self.photo_id != photo_id:
|
||||
if not photo_id:
|
||||
self.photo_id = ""
|
||||
|
||||
@@ -29,6 +29,7 @@ from mautrix.client import Client
|
||||
from mautrix.errors import MatrixRequestError
|
||||
from mautrix.types import UserID
|
||||
from mautrix.bridge import BaseUser
|
||||
from mautrix.util.logging import TraceLogger
|
||||
|
||||
from .types import TelegramID
|
||||
from .db import User as DBUser
|
||||
@@ -45,7 +46,7 @@ SearchResult = NewType('SearchResult', Tuple['pu.Puppet', int])
|
||||
|
||||
|
||||
class User(AbstractUser, BaseUser):
|
||||
log: logging.Logger = logging.getLogger("mau.user")
|
||||
log: TraceLogger = logging.getLogger("mau.user")
|
||||
by_mxid: Dict[str, 'User'] = {}
|
||||
by_tgid: Dict[int, 'User'] = {}
|
||||
|
||||
@@ -343,12 +344,14 @@ class User(AbstractUser, BaseUser):
|
||||
entity = dialog.entity
|
||||
if isinstance(entity, ChatForbidden):
|
||||
self.log.warning(f"Ignoring forbidden chat {entity} while syncing")
|
||||
continue
|
||||
elif isinstance(entity, Chat) and (entity.deactivated or entity.left):
|
||||
self.log.warning(f"Ignoring deactivated or left chat {entity} while syncing")
|
||||
continue
|
||||
elif isinstance(entity, TLUser) and not config["bridge.sync_direct_chats"]:
|
||||
self.log.trace(f"Ignoring user {entity.id} while syncing")
|
||||
continue
|
||||
portal = po.Portal.get_by_entity(entity)
|
||||
portal = po.Portal.get_by_entity(entity, receiver_id=self.tgid)
|
||||
self.portals[portal.tgid_full] = portal
|
||||
creators.append(
|
||||
portal.create_matrix_room(self, entity, invites=[self.mxid],
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
#
|
||||
# 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 mautrix.util.color_log import ColorFormatter as BaseColorFormatter, PREFIX, MXID_COLOR, RESET
|
||||
from mautrix.util.logging.color import (ColorFormatter as BaseColorFormatter,
|
||||
PREFIX, MXID_COLOR, RESET)
|
||||
|
||||
TELETHON_COLOR = PREFIX + "35;1m" # magenta
|
||||
TELETHON_MODULE_COLOR = PREFIX + "35m"
|
||||
|
||||
@@ -18,6 +18,7 @@ from io import BytesIO
|
||||
import time
|
||||
import logging
|
||||
import asyncio
|
||||
import tempfile
|
||||
|
||||
import magic
|
||||
from sqlalchemy.exc import IntegrityError, InvalidRequestError
|
||||
@@ -44,12 +45,8 @@ except ImportError:
|
||||
|
||||
try:
|
||||
from moviepy.editor import VideoFileClip
|
||||
import random
|
||||
import string
|
||||
import os
|
||||
import mimetypes
|
||||
except ImportError:
|
||||
VideoFileClip = random = string = os = mimetypes = None
|
||||
VideoFileClip = None
|
||||
|
||||
try:
|
||||
from nio.crypto import encrypt_attachment
|
||||
@@ -80,32 +77,23 @@ def convert_image(file: bytes, source_mime: str = "image/webp", target_type: str
|
||||
return source_mime, file, None, None
|
||||
|
||||
|
||||
def _temp_file_name(ext: str) -> str:
|
||||
return ("/tmp/mxtg-video-"
|
||||
+ "".join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10))
|
||||
+ ext)
|
||||
|
||||
|
||||
def _read_video_thumbnail(data: bytes, video_ext: str = "mp4", frame_ext: str = "png",
|
||||
max_size: Tuple[int, int] = (1024, 720)) -> Tuple[bytes, int, int]:
|
||||
# We don't have any way to read the video from memory, so save it to disk.
|
||||
temp_file = _temp_file_name(video_ext)
|
||||
with open(temp_file, "wb") as file:
|
||||
with tempfile.NamedTemporaryFile(prefix="mxtg_video_", suffix=f".{video_ext}") as file:
|
||||
# We don't have any way to read the video from memory, so save it to disk.
|
||||
file.write(data)
|
||||
|
||||
# Read temp file and get frame
|
||||
clip = VideoFileClip(temp_file)
|
||||
frame = clip.get_frame(0)
|
||||
# Read temp file and get frame
|
||||
frame = VideoFileClip(file.name).get_frame(0)
|
||||
|
||||
# Convert to png and save to BytesIO
|
||||
image = Image.fromarray(frame).convert("RGBA")
|
||||
|
||||
thumbnail_file = BytesIO()
|
||||
if max_size:
|
||||
image.thumbnail(max_size, Image.ANTIALIAS)
|
||||
image.save(thumbnail_file, frame_ext)
|
||||
|
||||
os.remove(temp_file)
|
||||
|
||||
w, h = image.size
|
||||
return thumbnail_file.getvalue(), w, h
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#
|
||||
# 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 typing import Optional, List, AsyncGenerator, Union, Awaitable, DefaultDict, Tuple
|
||||
from typing import Optional, List, AsyncGenerator, Union, Awaitable, DefaultDict, Tuple, cast
|
||||
from collections import defaultdict
|
||||
import hashlib
|
||||
import asyncio
|
||||
@@ -35,6 +35,7 @@ from telethon import utils, helpers
|
||||
|
||||
from mautrix.appservice import IntentAPI
|
||||
from mautrix.types import ContentURI, EncryptedFile
|
||||
from mautrix.util.logging import TraceLogger
|
||||
|
||||
from ..tgclient import MautrixTelegramClient
|
||||
from ..db import TelegramFile as DBTelegramFile
|
||||
@@ -44,7 +45,7 @@ try:
|
||||
except ImportError:
|
||||
async_encrypt_attachment = None
|
||||
|
||||
log: logging.Logger = logging.getLogger("mau.util")
|
||||
log: TraceLogger = cast(TraceLogger, logging.getLogger("mau.util"))
|
||||
|
||||
TypeLocation = Union[Document, InputDocumentFileLocation, InputPeerPhotoFileLocation,
|
||||
InputFileLocation, InputPhotoFileLocation]
|
||||
@@ -102,7 +103,7 @@ class UploadSender:
|
||||
|
||||
async def _next(self, data: bytes) -> None:
|
||||
self.request.bytes = data
|
||||
log.debug(f"Sending file part {self.request.file_part}/{self.part_count}"
|
||||
log.trace(f"Sending file part {self.request.file_part}/{self.part_count}"
|
||||
f" with {len(data)} bytes")
|
||||
await self.sender.send(self.request)
|
||||
self.request.file_part += self.stride
|
||||
@@ -236,7 +237,7 @@ class ParallelTransferrer:
|
||||
break
|
||||
yield data
|
||||
part += 1
|
||||
log.debug(f"Part {part} downloaded")
|
||||
log.trace(f"Part {part} downloaded")
|
||||
|
||||
log.debug("Parallel download finished, cleaning up connections")
|
||||
await self._cleanup()
|
||||
|
||||
+1
-1
@@ -4,6 +4,6 @@ ruamel.yaml>=0.15.35,<0.17
|
||||
python-magic>=0.4,<0.5
|
||||
commonmark>=0.8,<0.10
|
||||
aiohttp>=3,<4
|
||||
mautrix==0.5.0.beta13
|
||||
mautrix==0.5.0rc1
|
||||
telethon>=1.13,<1.14
|
||||
telethon-session-sqlalchemy>=0.2.14,<0.3
|
||||
|
||||
Reference in New Issue
Block a user