Compare commits

...

9 Commits

Author SHA1 Message Date
Tulir Asokan cead705c21 Bump version to 0.5.0rc3 2019-02-16 20:04:40 +02:00
Tulir Asokan e5a2afee37 Improve Matrix representation of Telegram polls 2019-02-16 19:55:27 +02:00
Tulir Asokan f2efb235eb Add command to vote in polls. Fixes #257 2019-02-16 19:47:38 +02:00
Tulir Asokan ffc1a5ad8f Show Telegram polls in Matrix (no voting yet. ref #257) 2019-02-16 17:43:23 +02:00
Tulir Asokan 1c3764b099 Fix saving user portals and contacts. Fixes #284 2019-02-16 17:29:14 +02:00
Tulir Asokan 5af045844e Make max photo size before sending as file configurable. Fixes #141 2019-02-16 17:14:02 +02:00
Tulir Asokan be255ec7af Fix bridging large images to Telegram 2019-02-16 17:08:07 +02:00
Tulir Asokan 7f7dec4e80 Fix bridging documents without thumbnails to Matrix 2019-02-16 17:07:58 +02:00
Tulir Asokan 8a6687d00c Use uvloop if installed 2019-02-16 17:07:19 +02:00
14 changed files with 194 additions and 87 deletions
+2
View File
@@ -144,6 +144,8 @@ bridge:
# Use inline images instead of a separate message for the caption.
# N.B. Inline images are not supported on all clients (e.g. Riot iOS).
inline_images: false
# Maximum size of image in megabytes before sending to Telegram as a document.
image_as_file_size: 10
# Whether to bridge Telegram bot messages as m.notices or m.texts.
bot_messages_as_notices: true
+1 -1
View File
@@ -1,2 +1,2 @@
__version__ = "0.5.0rc2"
__version__ = "0.5.0rc3"
__author__ = "Tulir Asokan <tulir@maunium.net>"
+8
View File
@@ -82,6 +82,14 @@ session_container = AlchemySessionContainer(engine=db_engine, session=db_session
manage_tables=False)
session_container.core_mode = True
try:
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
log.debug("Using uvloop for asyncio")
except ImportError:
pass
loop = asyncio.get_event_loop() # type: asyncio.AbstractEventLoop
state_store = SQLStateStore()
+12 -6
View File
@@ -22,7 +22,7 @@ import commonmark
from telethon.errors import FloodWaitError
from ..types import MatrixRoomID
from ..types import MatrixRoomID, MatrixEventID
from ..util import format_duration
from .. import user as u, context as c
@@ -60,8 +60,9 @@ md_renderer = HtmlEscapingRenderer()
class CommandEvent:
def __init__(self, processor: 'CommandProcessor', room: MatrixRoomID, sender: u.User,
command: str, args: List[str], is_management: bool, is_portal: bool) -> None:
def __init__(self, processor: 'CommandProcessor', room: MatrixRoomID, event: MatrixEventID,
sender: u.User, command: str, args: List[str], is_management: bool,
is_portal: bool) -> None:
self.az = processor.az
self.log = processor.log
self.loop = processor.loop
@@ -70,6 +71,7 @@ class CommandEvent:
self.public_website = processor.public_website
self.command_prefix = processor.command_prefix
self.room_id = room
self.event_id = event
self.sender = sender
self.command = command
self.args = args
@@ -89,6 +91,9 @@ class CommandEvent:
html = message
return self.az.intent.send_notice(self.room_id, message, html=html)
def mark_read(self) -> Awaitable[Dict]:
return self.az.intent.mark_read(self.room_id, self.event_id)
class CommandHandler:
def __init__(self, handler: Callable[[CommandEvent], Awaitable[Dict]], needs_auth: bool,
@@ -175,9 +180,10 @@ class CommandProcessor:
self.public_website = context.public_website
self.command_prefix = self.config["bridge.command_prefix"]
async def handle(self, room: MatrixRoomID, sender: u.User, command: str, args: List[str],
is_management: bool, is_portal: bool) -> Optional[Dict]:
evt = CommandEvent(self, room, sender, command, args, is_management, is_portal)
async def handle(self, room: MatrixRoomID, event_id: MatrixEventID, sender: u.User,
command: str, args: List[str], is_management: bool, is_portal: bool
) -> Optional[Dict]:
evt = CommandEvent(self, room, event_id, sender, command, args, is_management, is_portal)
orig_command = command
command = command.lower()
try:
+1 -1
View File
@@ -19,7 +19,7 @@ from typing import Dict, Callable, Optional
from ...types import MatrixRoomID
from ... import portal as po
from .. import command_handler, CommandEvent, SECTION_PORTAL_MANAGEMENT
from .util import user_has_power_level, get_initial_state
from .util import user_has_power_level
async def _get_portal_and_check_permission(evt: CommandEvent, permission: str,
@@ -22,7 +22,7 @@ from telethon.tl.types import Authorization
from telethon.tl.functions.account import (UpdateUsernameRequest, GetAuthorizationsRequest,
ResetAuthorizationRequest)
from mautrix_telegram.commands import command_handler, CommandEvent, SECTION_AUTH
from .. import command_handler, CommandEvent, SECTION_AUTH
@command_handler(needs_auth=True,
+3 -3
View File
@@ -23,9 +23,9 @@ from telethon.errors import (
PhoneNumberAppSignupForbiddenError, PhoneNumberBannedError, PhoneNumberFloodError,
PhoneNumberOccupiedError, PhoneNumberUnoccupiedError, SessionPasswordNeededError)
from mautrix_telegram.commands import command_handler, CommandEvent, SECTION_AUTH
from mautrix_telegram import puppet as pu, user as u
from mautrix_telegram.util import format_duration, ignore_coro
from ... import puppet as pu, user as u
from ...commands import command_handler, CommandEvent, SECTION_AUTH
from ...util import format_duration, ignore_coro
@command_handler(needs_auth=False,
+83 -34
View File
@@ -19,18 +19,21 @@ import codecs
import base64
import re
from telethon.errors import (InviteHashInvalidError, InviteHashExpiredError,
from telethon.errors import (InviteHashInvalidError, InviteHashExpiredError, OptionsTooMuchError,
UserAlreadyParticipantError)
from telethon.tl.types import User as TLUser, TypeUpdates, MessageMediaGame
from telethon.tl.patched import Message
from telethon.tl.types import (User as TLUser, TypeUpdates, MessageMediaGame, MessageMediaPoll,
TypePeer)
from telethon.tl.types.messages import BotCallbackAnswer
from telethon.tl.functions.messages import (ImportChatInviteRequest, CheckChatInviteRequest,
GetBotCallbackAnswerRequest)
GetBotCallbackAnswerRequest, SendVoteRequest)
from telethon.tl.functions.channels import JoinChannelRequest
from mautrix_telegram import puppet as pu, portal as po
from mautrix_telegram.db import Message as DBMessage
from mautrix_telegram.types import TelegramID
from mautrix_telegram.commands import command_handler, CommandEvent, SECTION_MISC, SECTION_CREATING_PORTALS
from ... import puppet as pu, portal as po
from ...abstract_user import AbstractUser
from ...db import Message as DBMessage
from ...types import TelegramID
from ...commands import command_handler, CommandEvent, SECTION_MISC, SECTION_CREATING_PORTALS
@command_handler(help_section=SECTION_MISC,
@@ -167,6 +170,45 @@ async def sync(evt: CommandEvent) -> Optional[Dict]:
PEER_TYPE_CHAT = b"g"
class MessageIDError(ValueError):
def __init__(self, message: str) -> None:
super().__init__(message)
self.message = message
async def _parse_encoded_msgid(user: AbstractUser, enc_id: str, type_name: str
) -> Tuple[TypePeer, Message]:
try:
enc_id += (4 - len(enc_id) % 4) * "="
enc_id = base64.b64decode(enc_id)
peer_type, enc_id = bytes([enc_id[0]]), enc_id[1:]
tgid = TelegramID(int(codecs.encode(enc_id[0:5], "hex_codec"), 16))
msg_id = TelegramID(int(codecs.encode(enc_id[5:10], "hex_codec"), 16))
space = None
if peer_type == PEER_TYPE_CHAT:
space = TelegramID(int(codecs.encode(enc_id[10:15], "hex_codec"), 16))
except ValueError as e:
raise MessageIDError(f"Invalid {type_name} ID (format)") from e
if peer_type == PEER_TYPE_CHAT:
orig_msg = DBMessage.get_by_tgid(msg_id, space)
if not orig_msg:
raise MessageIDError(f"Invalid {type_name} ID (original message not found in db)")
new_msg = DBMessage.get_by_mxid(orig_msg.mxid, orig_msg.mx_room, user.tgid)
if not new_msg:
raise MessageIDError(f"Invalid {type_name} ID (your copy of message not found in db)")
msg_id = new_msg.tgid
try:
peer = await user.client.get_input_entity(tgid)
except ValueError as e:
raise MessageIDError(f"Invalid {type_name} ID (chat not found)") from e
msg = await user.client.get_messages(entity=peer, ids=msg_id)
if not msg:
raise MessageIDError(f"Invalid {type_name} ID (message not found)")
return peer, msg
@command_handler(help_section=SECTION_MISC,
help_args="<_play ID_>",
help_text="Play a Telegram game.")
@@ -179,38 +221,45 @@ async def play(evt: CommandEvent) -> Optional[Dict]:
return await evt.reply("Bots can't play games :(")
try:
play_id = evt.args[0]
play_id += (4 - len(play_id) % 4) * "="
play_id = base64.b64decode(play_id)
peer_type, play_id = bytes([play_id[0]]), play_id[1:]
tgid = TelegramID(int(codecs.encode(play_id[0:5], "hex_codec"), 16))
msg_id = TelegramID(int(codecs.encode(play_id[5:10], "hex_codec"), 16))
space = None
if peer_type == PEER_TYPE_CHAT:
space = TelegramID(int(codecs.encode(play_id[10:15], "hex_codec"), 16))
except ValueError:
return await evt.reply("Invalid play ID (format)")
peer, msg = await _parse_encoded_msgid(evt.sender, evt.args[0], type_name="play")
except MessageIDError as e:
return await evt.reply(e.message)
if peer_type == PEER_TYPE_CHAT:
orig_msg = DBMessage.get_by_tgid(msg_id, space)
if not orig_msg:
return await evt.reply("Invalid play ID (original message not found in db)")
new_msg = DBMessage.get_by_mxid(orig_msg.mxid, orig_msg.mx_room, evt.sender.tgid)
if not new_msg:
return await evt.reply("Invalid play ID (your copy of message not found in db)")
msg_id = new_msg.tgid
try:
peer = await evt.sender.client.get_input_entity(tgid)
except ValueError:
return await evt.reply("Invalid play ID (chat not found)")
msg = await evt.sender.client.get_messages(entity=peer, ids=msg_id)
if not msg or not isinstance(msg.media, MessageMediaGame):
if not isinstance(msg.media, MessageMediaGame):
return await evt.reply("Invalid play ID (message doesn't look like a game)")
game = await evt.sender.client(GetBotCallbackAnswerRequest(peer=peer, msg_id=msg_id, game=True))
game = await evt.sender.client(GetBotCallbackAnswerRequest(peer=peer, msg_id=msg.id, game=True))
if not isinstance(game, BotCallbackAnswer):
return await evt.reply("Game request response invalid")
await evt.reply(f"Click [here]({game.url}) to play {msg.media.game.title}:\n\n"
f"{msg.media.game.description}")
@command_handler(help_section=SECTION_MISC,
help_args="<_poll ID_> <_choice ID_>",
help_text="Vote in a Telegram poll.")
async def vote(evt: CommandEvent) -> Optional[Dict]:
if len(evt.args) < 2:
return await evt.reply("**Usage:** `$cmdprefix+sp vote <poll ID> <choice ID>`")
elif not await evt.sender.is_logged_in():
return await evt.reply("You must be logged in with a real account to vote in polls.")
elif evt.sender.is_bot:
return await evt.reply("Bots can't vote in polls :(")
try:
peer, msg = await _parse_encoded_msgid(evt.sender, evt.args[0], type_name="poll")
except MessageIDError as e:
return await evt.reply(e.message)
if not isinstance(msg.media, MessageMediaPoll):
return await evt.reply("Invalid poll ID (message doesn't look like a poll)")
options = [base64.b64decode(option + (4 - len(option) % 4) * "=")
for option in evt.args[1:]]
try:
resp = await evt.sender.client(SendVoteRequest(peer=peer, msg_id=msg.id, options=options))
except OptionsTooMuchError:
return await evt.reply("You passed too many options.")
# TODO use response
return await evt.mark_read()
+1
View File
@@ -206,6 +206,7 @@ class Config(DictWithRecursion):
copy("bridge.sync_with_custom_puppets")
copy("bridge.telegram_link_preview")
copy("bridge.inline_images")
copy("bridge.image_as_file_size")
copy("bridge.bot_messages_as_notices")
if isinstance(self["bridge.bridge_notices"], bool):
+1 -1
View File
@@ -18,7 +18,7 @@ from sqlalchemy import Column, ForeignKey, ForeignKeyConstraint, Integer, String
from sqlalchemy.engine.result import RowProxy
from typing import Optional, Iterable, Tuple
from ..types import MatrixUserID, MatrixRoomID, TelegramID
from ..types import MatrixUserID, TelegramID
from .base import Base
+1 -1
View File
@@ -248,7 +248,7 @@ class MatrixHandler:
# Not enough values to unpack, i.e. no arguments
command = text
args = []
await self.commands.handle(room, sender, command, args, is_management,
await self.commands.handle(room, event_id, sender, command, args, is_management,
is_portal=portal is not None)
@staticmethod
+62 -29
View File
@@ -38,17 +38,17 @@ from telethon.tl.functions.messages import (
EditChatPhotoRequest, EditChatTitleRequest, ExportChatInviteRequest, GetFullChatRequest,
UpdatePinnedMessageRequest, MigrateChatRequest, SetTypingRequest, EditChatAboutRequest)
from telethon.tl.functions.channels import (
CreateChannelRequest, EditAdminRequest, EditBannedRequest, EditPhotoRequest,
EditTitleRequest, GetParticipantsRequest, InviteToChannelRequest,
JoinChannelRequest, LeaveChannelRequest, UpdateUsernameRequest)
CreateChannelRequest, EditAdminRequest, EditBannedRequest, EditPhotoRequest, EditTitleRequest,
GetParticipantsRequest, InviteToChannelRequest, JoinChannelRequest, LeaveChannelRequest,
UpdateUsernameRequest)
from telethon.tl.functions.messages import ReadHistoryRequest as ReadMessageHistoryRequest
from telethon.tl.functions.channels import ReadHistoryRequest as ReadChannelHistoryRequest
from telethon.errors import ChatAdminRequiredError, ChatNotModifiedError
from telethon.tl.patched import Message, MessageService
from telethon.tl.types import (
Channel, ChatAdminRights, ChatBannedRights, ChannelFull, ChannelParticipantAdmin,
ChannelParticipantCreator, ChannelParticipantsRecent, ChannelParticipantsSearch, Chat,
ChatFull, ChatInviteEmpty, ChatParticipantAdmin, ChatParticipantCreator, ChatPhoto,
ChannelParticipantCreator, ChannelParticipantsRecent, ChannelParticipantsSearch, Chat, ChatFull,
ChatInviteEmpty, ChatParticipantAdmin, ChatParticipantCreator, ChatPhoto, Poll, PollAnswer,
DocumentAttributeFilename, DocumentAttributeImageSize, DocumentAttributeSticker,
DocumentAttributeVideo, FileLocation, GeoPoint, InputChannel, InputChatUploadedPhoto,
InputPeerChannel, InputPeerChat, InputPeerUser, InputUser, InputUserSelf,
@@ -56,12 +56,12 @@ from telethon.tl.types import (
MessageActionChatDeletePhoto, MessageActionChatDeleteUser, MessageActionChatEditPhoto,
MessageActionChatEditTitle, MessageActionChatJoinedByLink, MessageActionChatMigrateTo,
MessageActionPinMessage, MessageActionGameScore, MessageMediaContact, MessageMediaDocument,
MessageMediaGeo, MessageMediaPhoto, MessageMediaUnsupported, MessageMediaGame, PeerChannel,
PeerChat, PeerUser, Photo, PhotoCachedSize, SendMessageCancelAction, SendMessageTypingAction,
TypeChannelParticipant, TypeChat, TypeChatParticipant, TypeDocumentAttribute, TypeInputPeer,
TypeMessageAction, TypeMessageEntity, TypePeer, TypePhotoSize, TypeUpdates, TypeUser, PhotoSize,
TypeUserFull, UpdateChatUserTyping, UpdateNewChannelMessage, UpdateNewMessage, UpdateUserTyping,
User, UserFull, MessageEntityPre)
MessageMediaGeo, MessageMediaPhoto, MessageMediaUnsupported, MessageMediaGame, MessageMediaPoll,
PeerChannel, PeerChat, PeerUser, Photo, PhotoCachedSize, SendMessageCancelAction,
SendMessageTypingAction, TypeChannelParticipant, TypeChat, TypeChatParticipant,
TypeDocumentAttribute, TypeInputPeer, TypeMessageAction, TypeMessageEntity, TypePeer,
TypePhotoSize, TypeUpdates, TypeUser, PhotoSize, TypeUserFull, UpdateChatUserTyping,
UpdateNewChannelMessage, UpdateNewMessage, UpdateUserTyping, User, UserFull, MessageEntityPre)
from mautrix_appservice import MatrixRequestError, IntentError, AppService, IntentAPI
from .types import MatrixEventID, MatrixRoomID, MatrixUserID, TelegramID
@@ -610,7 +610,10 @@ class Portal:
return False
@staticmethod
def _get_largest_photo_size(photo: Union[Photo, List[TypePhotoSize]]) -> TypePhotoSize:
def _get_largest_photo_size(photo: Union[Photo, List[TypePhotoSize]]
) -> Optional[TypePhotoSize]:
if not photo:
return None
return max(photo.sizes if isinstance(photo, Photo) else photo, key=(lambda photo2: (
len(photo2.bytes) if not isinstance(photo2, PhotoSize) else photo2.size)))
@@ -981,7 +984,9 @@ class Portal:
caption = message["body"] if message["body"].lower() != file_name.lower() else None
media = await client.upload_file_direct(file, mime, attributes, file_name)
media = await client.upload_file_direct(
file, mime, attributes, file_name,
max_image_size=config["bridge.image_as_file_size"] * 1000 ** 2)
lock = self.require_send_lock(sender_id)
async with lock:
response = await client.send_media(self.peer, media, reply_to=reply_to,
@@ -1406,7 +1411,7 @@ class Portal:
attrs = self._parse_telegram_document_attributes(document.attributes)
thumb = self._get_largest_photo_size(document.thumbs)
if not isinstance(thumb, (PhotoSize, PhotoCachedSize)):
if thumb and not isinstance(thumb, (PhotoSize, PhotoCachedSize)):
self.log.debug(f"Unsupported thumbnail type {type(thumb)}")
thumb = None
file = await util.transfer_file_to_matrix(source.client, intent, document, thumb,
@@ -1497,30 +1502,57 @@ class Portal:
"net.maunium.telegram.unsupported": True,
}, timestamp=evt.date, external_url=self.get_external_url(evt))
async def handle_telegram_poll(self, source: 'AbstractUser', intent: IntentAPI, evt: Message,
relates_to: dict) -> dict:
poll = evt.media.poll # type: Poll
poll_id = self._encode_msgid(source, evt)
def enc(answer: PollAnswer) -> str:
return base64.b64encode(answer.option).decode("utf-8").rstrip("=")
text = (f"Poll ID {poll_id}: {poll.question}\n"
+ "\n".join(f"* {enc(answer)}: {answer.text}" for answer in poll.answers) +
"\n"
f"Vote with !tg vote <poll ID> <choice ID>")
html = (f"<strong>Poll</strong> ID <code>{poll_id}</code>: {poll.question}<br/>\n"
f"<ul>"
+ "\n".join(f"<li><code>{enc(answer)}</code>: {answer.text}</li>"
for answer in poll.answers) +
"</ul>\n"
f"Vote with <code>!tg vote &lt;poll ID&gt; &lt;choice ID&gt;</code>")
await intent.set_typing(self.mxid, is_typing=False)
return await intent.send_text(self.mxid, text, html=html, relates_to=relates_to,
msgtype="m.text", timestamp=evt.date,
external_url=self.get_external_url(evt))
@staticmethod
def _int_to_bytes(i: int) -> bytes:
hex_value = "{0:010x}".format(i)
return codecs.decode(hex_value, "hex_codec")
async def handle_telegram_game(self, source: 'AbstractUser', intent: IntentAPI,
evt: Message, _: dict = None):
game = evt.media.game
def _encode_msgid(self, source: 'AbstractUser', evt: Message) -> str:
if self.peer_type == "channel":
play_id = base64.b64encode(b"c"
+ self._int_to_bytes(self.tgid)
+ self._int_to_bytes(evt.id))
play_id = (b"c"
+ self._int_to_bytes(self.tgid)
+ self._int_to_bytes(evt.id))
elif self.peer_type == "chat":
play_id = base64.b64encode(b"g"
+ self._int_to_bytes(self.tgid)
+ self._int_to_bytes(evt.id)
+ self._int_to_bytes(source.tgid))
play_id = (b"g"
+ self._int_to_bytes(self.tgid)
+ self._int_to_bytes(evt.id)
+ self._int_to_bytes(source.tgid))
elif self.peer_type == "user":
play_id = base64.b64encode(b"u"
+ self._int_to_bytes(self.tgid)
+ self._int_to_bytes(evt.id))
play_id = (b"u"
+ self._int_to_bytes(self.tgid)
+ self._int_to_bytes(evt.id))
else:
raise ValueError("Portal has invalid peer type")
play_id = play_id.decode("utf-8").rstrip("=")
return base64.b64encode(play_id).decode("utf-8").rstrip("=")
async def handle_telegram_game(self, source: 'AbstractUser', intent: IntentAPI,
evt: Message, relates_to: dict = None):
game = evt.media.game
play_id = self._encode_msgid(source, evt)
command = f"!tg play {play_id}"
override_text = f"Run {command} in your bridge management room to play {game.title}"
override_entities = [MessageEntityPre(offset=len("Run "), length=len(command), language="")]
@@ -1625,7 +1657,7 @@ class Portal:
await sender.update_info(source, entity)
allowed_media = (MessageMediaPhoto, MessageMediaDocument, MessageMediaGeo, MessageMediaGame,
MessageMediaUnsupported)
MessageMediaPoll, MessageMediaUnsupported)
media = evt.media if hasattr(evt, "media") and isinstance(evt.media,
allowed_media) else None
intent = sender.intent if sender else self.main_intent
@@ -1637,6 +1669,7 @@ class Portal:
MessageMediaPhoto: self.handle_telegram_photo,
MessageMediaDocument: self.handle_telegram_document,
MessageMediaGeo: self.handle_telegram_location,
MessageMediaPoll: self.handle_telegram_poll,
MessageMediaUnsupported: self.handle_telegram_unsupported,
MessageMediaGame: self.handle_telegram_game,
}[type(media)](source, intent, evt,
+2 -2
View File
@@ -27,11 +27,11 @@ from telethon.tl.patched import Message
class MautrixTelegramClient(TelegramClient):
async def upload_file_direct(self, file: bytes, mime_type: str = None,
attributes: List[TypeDocumentAttribute] = None,
file_name: str = None
file_name: str = None, max_image_size: float = 10 * 1000 ** 2,
) -> Union[InputMediaUploadedDocument, InputMediaUploadedPhoto]:
file_handle = await super().upload_file(file, file_name=file_name, use_cache=False)
if mime_type == "image/png" or mime_type == "image/jpeg":
if (mime_type == "image/png" or mime_type == "image/jpeg") and len(file) < max_image_size:
return InputMediaUploadedPhoto(file_handle)
else:
attributes = attributes or []
+16 -8
View File
@@ -102,7 +102,9 @@ class User(AbstractUser):
@property
def db_contacts(self) -> Iterable[TelegramID]:
return (puppet.id for puppet in self.contacts)
return (puppet.id
for puppet in self.contacts
if puppet)
@db_contacts.setter
def db_contacts(self, contacts: Iterable[TelegramID]) -> None:
@@ -110,7 +112,9 @@ class User(AbstractUser):
@property
def db_portals(self) -> Iterable[Tuple[TelegramID, TelegramID]]:
return (portal.tgid_full for portal in self.portals.values() if not portal.deleted)
return (portal.tgid_full
for portal in self.portals.values()
if portal and not portal.deleted)
@db_portals.setter
def db_portals(self, portals: Iterable[Tuple[TelegramID, TelegramID]]) -> None:
@@ -132,9 +136,13 @@ class User(AbstractUser):
contacts=self.db_contacts, saved_contacts=self.saved_contacts,
portals=self.db_portals)
def save(self) -> None:
def save(self, contacts: bool = False, portals: bool = False) -> None:
self.db_instance.update(tgid=self.tgid, tg_username=self.username, tg_phone=self.phone,
saved_contacts=self.saved_contacts)
if contacts:
self.db_instance.contacts = self.db_contacts
if portals:
self.db_instance.portals = self.db_portals
def delete(self, delete_db: bool = True) -> None:
try:
@@ -240,7 +248,7 @@ class User(AbstractUser):
pass
self.portals = {}
self.contacts = []
self.save()
self.save(portals=True, contacts=True)
if self.tgid:
try:
del self.by_tgid[self.tgid]
@@ -295,7 +303,7 @@ class User(AbstractUser):
creators.append(
portal.create_matrix_room(self, entity, invites=[self.mxid],
synchronous=synchronous_create))
self.save()
self.save(portals=True)
await asyncio.gather(*creators, loop=self.loop)
def register_portal(self, portal: po.Portal) -> None:
@@ -305,12 +313,12 @@ class User(AbstractUser):
except KeyError:
pass
self.portals[portal.tgid_full] = portal
self.save()
self.save(portals=True)
def unregister_portal(self, portal: po.Portal) -> None:
try:
del self.portals[portal.tgid_full]
self.save()
self.save_portals()
except KeyError:
pass
@@ -335,7 +343,7 @@ class User(AbstractUser):
puppet = pu.Puppet.get(user.id)
await puppet.update_info(self, user)
self.contacts.append(puppet)
self.save()
self.save(contacts=True)
# endregion
# region Class instance lookup