Add support for joining chats and initiating private chats

This commit is contained in:
Tulir Asokan
2018-01-28 14:33:40 +02:00
parent 831851f118
commit f83f7870c8
6 changed files with 122 additions and 36 deletions

View File

@@ -105,18 +105,17 @@ does not do this automatically.~~
* [ ] Public channel username changes
* [x] Initial chat metadata
* [x] Supergroup upgrade
* Initiating chats
* Misc
* [x] Automatic portal creation for groups/channels at startup
* [x] Automatic portal creation for groups/channels when receiving invite/message
* [ ] Private chat creation by inviting Telegram user to new room
* [ ] Searching for Telegram users using management commands
* Misc
* [ ] Use optional bot to relay messages for unauthenticated Matrix users
* [ ] Joining public channels/supergroups using room aliases
* Commands
* [x] Logging in and out (`login` + code entering, `logout`)
* [ ] Registering (`register`)
* [ ] Searching for users (`search`)
* [ ] Starting private chats (`pm`)
* [x] Searching for users (`search`)
* [x] Starting private chats (`pm`)
* [x] Joining chats with invite links (`join`)
* [ ] Creating a Telegram chat for an existing Matrix room (`create`)
* [ ] Upgrading the chat of a portal room into a supergroup (`upgrade`)

View File

@@ -187,9 +187,10 @@ class IntentAPI:
# region Room actions
def create_room(self, alias=None, is_public=False, name=None, topic=None, is_direct=False,
invitees=()):
invitees=(), initial_state=[]):
self._ensure_registered()
return self.client.create_room(alias, is_public, name, topic, is_direct, invitees)
return self.client.create_room(alias, is_public, name, topic, is_direct, invitees,
initial_state)
def invite(self, room_id, user_id):
self._ensure_joined(room_id)

View File

@@ -16,6 +16,11 @@
from contextlib import contextmanager
import markdown
from telethon.errors import *
from telethon.tl.types import *
from telethon.tl.functions.contacts import SearchRequest
from telethon.tl.functions.messages import ImportChatInviteRequest, CheckChatInviteRequest
from telethon.tl.functions.channels import JoinChannelRequest
from . import puppet as pu, portal as po
command_handlers = {}
@@ -37,7 +42,12 @@ class CommandHandler:
def handle(self, room, sender, command, args, is_management, is_portal):
with self.handler(sender, room, command, args, is_management, is_portal) as handle_command:
handle_command(self, sender, args)
try:
handle_command(self, sender, args)
except:
self.reply("Fatal error while handling command. Check logs for more details.")
self.log.exception(f"Fatal error handling command "
f"'$cmdprefix {command} {''.join(args)}' from {sender.mxid}")
@contextmanager
def handler(self, sender, room, command, args, is_management, is_portal):
@@ -62,9 +72,9 @@ class CommandHandler:
raise AttributeError("the reply function can only be used from within"
"the `CommandHandler.run` context manager")
message = message.replace("$cmdprefix", self.command_prefix)
message = message.replace("$cmdprefix+sp ",
"" if self._is_management else f"{self.command_prefix} ")
message = message.replace("$cmdprefix", self.command_prefix)
html = None
if render_markdown:
html = markdown.markdown(message, safe_mode="escape" if allow_html else False)
@@ -165,7 +175,7 @@ class CommandHandler:
return self.reply("Incorrect password.")
except:
self.log.exception()
return self.reply("Unhandled exception while sending password."
return self.reply("Unhandled exception while sending password. "
"Check console for more details.")
@command_handler
@@ -181,11 +191,83 @@ class CommandHandler:
@command_handler
def search(self, sender, args):
self.reply("Not yet implemented.")
if len(args) == 0:
return self.reply("**Usage:** `$cmdprefix+sp search [-r|--remote] <query>")
elif not sender.tgid:
return self.reply("This command requires you to be logged in.")
force_remote = False
if args[0] in {"-r", "--remote"}:
args.pop(0)
query = " ".join(args)
if len(query) < 5:
return self.reply("Minimum length of query for remote search is 5 characters.")
found = sender.client(SearchRequest(q=query, limit=10))
print(found)
# reply = ["**People:**", ""]
reply = ["**Results from Telegram server:**", ""]
for result in found.users:
puppet = pu.Puppet.get(result.id)
puppet.update_info(sender, result)
reply.append(
f"* [{puppet.displayname}](https://matrix.to/#/{puppet.mxid}): {puppet.id}")
# reply.extend(("", "**Chats:**", ""))
# for result in found.chats:
# reply.append(f"* {result.title}")
return self.reply("\n".join(reply))
@command_handler
def pm(self, sender, args):
self.reply("Not yet implemented.")
if len(args) == 0:
return self.reply("**Usage:** `$cmdprefix+sp pm <user identifier>")
elif not sender.tgid:
return self.reply("This command requires you to be logged in.")
user = sender.client.get_entity(args[0])
if not user:
return self.reply("User not found.")
elif not isinstance(user, User):
return self.reply("That doesn't seem to be a user.")
print(user)
def _strip_prefix(self, value, prefixes):
for prefix in prefixes:
if value.startswith(prefix):
return value[len(prefix):]
return value
@command_handler
def join(self, sender, args):
if len(args) == 0:
return self.reply("**Usage:** `$cmdprefix+sp join <invite link>")
elif not sender.tgid:
return self.reply("This command requires you to be logged in.")
regex = re.compile(r"(?:https?://)?t(?:elegram)?\.(?:dog|me)(?:joinchat/)?/(.+)")
arg = regex.match(args[0])
if not arg:
return self.reply("That doesn't look like a Telegram invite link.")
arg = arg.group(1)
if arg.startswith("joinchat/"):
invite_hash = arg[len("joinchat/"):]
try:
check = sender.client(CheckChatInviteRequest(invite_hash))
print(check)
except InviteHashInvalidError:
return self.reply("Invalid invite link.")
except InviteHashExpiredError:
return self.reply("Invite link expired.")
try:
updates = sender.client(ImportChatInviteRequest(invite_hash))
except UserAlreadyParticipantError:
return self.reply("You are already in that chat.")
else:
channel = sender.client.get_entity(arg)
if not channel:
return self.reply("Channel/supergroup not found.")
updates = sender.client(JoinChannelRequest(channel))
for chat in updates.chats:
portal = po.Portal.get_by_entity(chat)
portal.create_room(sender, chat, [sender.mxid])
@command_handler
def create(self, sender, args):
@@ -235,9 +317,12 @@ _**Telegram actions**: commands for using the bridge to interact with Telegram._
**logout** - Log out from Telegram.
**ping** - Check if you're logged into Telegram.
**search** [_-r|--remote_] <_query_> - Search your contacts or the Telegram servers for users.
**pm** <_id_> - Open a private chat with the given Telegram user ID.
**create** <_group/channel_> [_room ID_] - Create a Telegram chat of the given type for a Matrix room.
If the room ID is not specified, a chat for the current room is created.
**pm** <_identifier_> - Open a private chat with the given Telegram user. The identifier is either
the internal user ID, the username or the phone number.
**join** <_link_> - Join a chat with an invite link.
**create** <_group/channel_> [_room ID_] - Create a Telegram chat of the given type for a Matrix
room. If the room ID is not specified, a chat for the
current room is created.
**upgrade** - Upgrade a normal Telegram group to a supergroup.
"""
return self.reply(management_status + help)

View File

@@ -98,28 +98,25 @@ class Portal:
puppet = p.Puppet.get(self.tgid) if direct else None
intent = puppet.intent if direct else self.az.intent
power_level_requirement = 0 if self.peer_type == "chat" else 50
initial_power_levels = {
"ban": 100,
"events": {
"m.room.name": power_level_requirement,
"m.room.avatar": power_level_requirement,
"m.room.topic": 50,
"m.room.power_levels": 50,
"invite": power_level_requirement,
},
"users_default": 0,
}
# TODO set room alias if public channel.
room = intent.create_room(invitees=invites, name=title, is_direct=direct,
initial_state=[initial_power_levels])
room = intent.create_room(invitees=invites, name=title, is_direct=direct)
if not room:
raise Exception(f"Failed to create room for {self.tgid}")
self.mxid = room["room_id"]
self.by_mxid[self.mxid] = self
self.save()
power_level_requirement = 0 if self.peer_type == "chat" else 50
levels = self.main_intent.get_power_levels(self.mxid)
levels["ban"] = 100
levels["invite"] = 50
levels["events"]["m.room.name"] = power_level_requirement
levels["events"]["m.room.avatar"] = power_level_requirement
levels["events"]["m.room.topic"] = 50 if self.peer_type == "channel" else 100
levels["events"]["m.room.power_levels"] = 95
self.main_intent.set_power_levels(self.mxid, levels)
if not direct:
self.update_info(user, entity)
users, participants = self.get_users(user, entity)
@@ -397,11 +394,11 @@ class Portal:
def handle_telegram_action(self, source, sender, action):
if not self.mxid:
create_and_exit = [MessageActionChatCreate, MessageActionChannelCreate]
create_and_continue = [MessageActionChatAddUser, MessageActionChatJoinedByLink]
create_and_exit = (MessageActionChatCreate, MessageActionChannelCreate)
create_and_continue = (MessageActionChatAddUser, MessageActionChatJoinedByLink)
if isinstance(action, create_and_exit + create_and_continue):
self.create_room(source, invites=[source.mxid])
if isinstance(action, create_and_exit):
if not isinstance(action, create_and_continue):
return
if isinstance(action, MessageActionChatEditTitle):

View File

@@ -29,7 +29,8 @@ class Puppet:
def __init__(self, id=None, username=None, displayname=None, photo_id=None):
self.id = id
self.localpart = config.get("bridge.username_template", "telegram_{userid}").format(userid=self.id)
self.localpart = config.get("bridge.username_template", "telegram_{userid}").format(
userid=self.id)
hs = config["homeserver"]["domain"]
self.mxid = f"@{self.localpart}:{hs}"
self.username = username
@@ -75,7 +76,8 @@ class Puppet:
if not format:
return name
return config.get("bridge.displayname_template", "{displayname} (Telegram)").format(displayname=name)
return config.get("bridge.displayname_template", "{displayname} (Telegram)").format(
displayname=name)
def update_info(self, source, info):
changed = False

View File

@@ -97,9 +97,11 @@ class User:
self.connected = False
if self.tgid:
try:
del self.tgid[self.tgid]
del self.by_tgid[self.tgid]
except KeyError:
pass
self.tgid = None
self.save()
return self.client.log_out()
def send_message(self, entity, message, reply_to=None, entities=None, link_preview=True):