Add support for joining chats and initiating private chats
This commit is contained in:
@@ -105,18 +105,17 @@ does not do this automatically.~~
|
|||||||
* [ ] Public channel username changes
|
* [ ] Public channel username changes
|
||||||
* [x] Initial chat metadata
|
* [x] Initial chat metadata
|
||||||
* [x] Supergroup upgrade
|
* [x] Supergroup upgrade
|
||||||
* Initiating chats
|
* Misc
|
||||||
* [x] Automatic portal creation for groups/channels at startup
|
* [x] Automatic portal creation for groups/channels at startup
|
||||||
* [x] Automatic portal creation for groups/channels when receiving invite/message
|
* [x] Automatic portal creation for groups/channels when receiving invite/message
|
||||||
* [ ] Private chat creation by inviting Telegram user to new room
|
* [ ] 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
|
* [ ] Use optional bot to relay messages for unauthenticated Matrix users
|
||||||
* [ ] Joining public channels/supergroups using room aliases
|
* [ ] Joining public channels/supergroups using room aliases
|
||||||
* Commands
|
* Commands
|
||||||
* [x] Logging in and out (`login` + code entering, `logout`)
|
* [x] Logging in and out (`login` + code entering, `logout`)
|
||||||
* [ ] Registering (`register`)
|
* [ ] Registering (`register`)
|
||||||
* [ ] Searching for users (`search`)
|
* [x] Searching for users (`search`)
|
||||||
* [ ] Starting private chats (`pm`)
|
* [x] Starting private chats (`pm`)
|
||||||
|
* [x] Joining chats with invite links (`join`)
|
||||||
* [ ] Creating a Telegram chat for an existing Matrix room (`create`)
|
* [ ] Creating a Telegram chat for an existing Matrix room (`create`)
|
||||||
* [ ] Upgrading the chat of a portal room into a supergroup (`upgrade`)
|
* [ ] Upgrading the chat of a portal room into a supergroup (`upgrade`)
|
||||||
|
|||||||
@@ -187,9 +187,10 @@ class IntentAPI:
|
|||||||
# region Room actions
|
# region Room actions
|
||||||
|
|
||||||
def create_room(self, alias=None, is_public=False, name=None, topic=None, is_direct=False,
|
def create_room(self, alias=None, is_public=False, name=None, topic=None, is_direct=False,
|
||||||
invitees=()):
|
invitees=(), initial_state=[]):
|
||||||
self._ensure_registered()
|
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):
|
def invite(self, room_id, user_id):
|
||||||
self._ensure_joined(room_id)
|
self._ensure_joined(room_id)
|
||||||
|
|||||||
@@ -16,6 +16,11 @@
|
|||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
import markdown
|
import markdown
|
||||||
from telethon.errors import *
|
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 = {}
|
command_handlers = {}
|
||||||
|
|
||||||
@@ -37,7 +42,12 @@ class CommandHandler:
|
|||||||
|
|
||||||
def handle(self, room, sender, command, args, is_management, is_portal):
|
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:
|
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
|
@contextmanager
|
||||||
def handler(self, sender, room, command, args, is_management, is_portal):
|
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"
|
raise AttributeError("the reply function can only be used from within"
|
||||||
"the `CommandHandler.run` context manager")
|
"the `CommandHandler.run` context manager")
|
||||||
|
|
||||||
message = message.replace("$cmdprefix", self.command_prefix)
|
|
||||||
message = message.replace("$cmdprefix+sp ",
|
message = message.replace("$cmdprefix+sp ",
|
||||||
"" if self._is_management else f"{self.command_prefix} ")
|
"" if self._is_management else f"{self.command_prefix} ")
|
||||||
|
message = message.replace("$cmdprefix", self.command_prefix)
|
||||||
html = None
|
html = None
|
||||||
if render_markdown:
|
if render_markdown:
|
||||||
html = markdown.markdown(message, safe_mode="escape" if allow_html else False)
|
html = markdown.markdown(message, safe_mode="escape" if allow_html else False)
|
||||||
@@ -165,7 +175,7 @@ class CommandHandler:
|
|||||||
return self.reply("Incorrect password.")
|
return self.reply("Incorrect password.")
|
||||||
except:
|
except:
|
||||||
self.log.exception()
|
self.log.exception()
|
||||||
return self.reply("Unhandled exception while sending password."
|
return self.reply("Unhandled exception while sending password. "
|
||||||
"Check console for more details.")
|
"Check console for more details.")
|
||||||
|
|
||||||
@command_handler
|
@command_handler
|
||||||
@@ -181,11 +191,83 @@ class CommandHandler:
|
|||||||
|
|
||||||
@command_handler
|
@command_handler
|
||||||
def search(self, sender, args):
|
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
|
@command_handler
|
||||||
def pm(self, sender, args):
|
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
|
@command_handler
|
||||||
def create(self, sender, args):
|
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.
|
**logout** - Log out from Telegram.
|
||||||
**ping** - Check if you're logged into Telegram.
|
**ping** - Check if you're logged into Telegram.
|
||||||
**search** [_-r|--remote_] <_query_> - Search your contacts or the Telegram servers for users.
|
**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.
|
**pm** <_identifier_> - Open a private chat with the given Telegram user. The identifier is either
|
||||||
**create** <_group/channel_> [_room ID_] - Create a Telegram chat of the given type for a Matrix room.
|
the internal user ID, the username or the phone number.
|
||||||
If the room ID is not specified, a chat for the current room is created.
|
**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.
|
**upgrade** - Upgrade a normal Telegram group to a supergroup.
|
||||||
"""
|
"""
|
||||||
return self.reply(management_status + help)
|
return self.reply(management_status + help)
|
||||||
|
|||||||
+15
-18
@@ -98,28 +98,25 @@ class Portal:
|
|||||||
puppet = p.Puppet.get(self.tgid) if direct else None
|
puppet = p.Puppet.get(self.tgid) if direct else None
|
||||||
intent = puppet.intent if direct else self.az.intent
|
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.
|
# TODO set room alias if public channel.
|
||||||
room = intent.create_room(invitees=invites, name=title, is_direct=direct,
|
room = intent.create_room(invitees=invites, name=title, is_direct=direct)
|
||||||
initial_state=[initial_power_levels])
|
|
||||||
if not room:
|
if not room:
|
||||||
raise Exception(f"Failed to create room for {self.tgid}")
|
raise Exception(f"Failed to create room for {self.tgid}")
|
||||||
|
|
||||||
self.mxid = room["room_id"]
|
self.mxid = room["room_id"]
|
||||||
self.by_mxid[self.mxid] = self
|
self.by_mxid[self.mxid] = self
|
||||||
self.save()
|
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:
|
if not direct:
|
||||||
self.update_info(user, entity)
|
self.update_info(user, entity)
|
||||||
users, participants = self.get_users(user, entity)
|
users, participants = self.get_users(user, entity)
|
||||||
@@ -397,11 +394,11 @@ class Portal:
|
|||||||
|
|
||||||
def handle_telegram_action(self, source, sender, action):
|
def handle_telegram_action(self, source, sender, action):
|
||||||
if not self.mxid:
|
if not self.mxid:
|
||||||
create_and_exit = [MessageActionChatCreate, MessageActionChannelCreate]
|
create_and_exit = (MessageActionChatCreate, MessageActionChannelCreate)
|
||||||
create_and_continue = [MessageActionChatAddUser, MessageActionChatJoinedByLink]
|
create_and_continue = (MessageActionChatAddUser, MessageActionChatJoinedByLink)
|
||||||
if isinstance(action, create_and_exit + create_and_continue):
|
if isinstance(action, create_and_exit + create_and_continue):
|
||||||
self.create_room(source, invites=[source.mxid])
|
self.create_room(source, invites=[source.mxid])
|
||||||
if isinstance(action, create_and_exit):
|
if not isinstance(action, create_and_continue):
|
||||||
return
|
return
|
||||||
|
|
||||||
if isinstance(action, MessageActionChatEditTitle):
|
if isinstance(action, MessageActionChatEditTitle):
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ class Puppet:
|
|||||||
def __init__(self, id=None, username=None, displayname=None, photo_id=None):
|
def __init__(self, id=None, username=None, displayname=None, photo_id=None):
|
||||||
self.id = id
|
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"]
|
hs = config["homeserver"]["domain"]
|
||||||
self.mxid = f"@{self.localpart}:{hs}"
|
self.mxid = f"@{self.localpart}:{hs}"
|
||||||
self.username = username
|
self.username = username
|
||||||
@@ -75,7 +76,8 @@ class Puppet:
|
|||||||
|
|
||||||
if not format:
|
if not format:
|
||||||
return name
|
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):
|
def update_info(self, source, info):
|
||||||
changed = False
|
changed = False
|
||||||
|
|||||||
@@ -97,9 +97,11 @@ class User:
|
|||||||
self.connected = False
|
self.connected = False
|
||||||
if self.tgid:
|
if self.tgid:
|
||||||
try:
|
try:
|
||||||
del self.tgid[self.tgid]
|
del self.by_tgid[self.tgid]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
self.tgid = None
|
||||||
|
self.save()
|
||||||
return self.client.log_out()
|
return self.client.log_out()
|
||||||
|
|
||||||
def send_message(self, entity, message, reply_to=None, entities=None, link_preview=True):
|
def send_message(self, entity, message, reply_to=None, entities=None, link_preview=True):
|
||||||
|
|||||||
Reference in New Issue
Block a user