Minor fixes and preparation for proper permission checking in intent API

This commit is contained in:
Tulir Asokan
2018-01-27 18:50:19 +02:00
parent 35d425c21d
commit aea82daf1b
3 changed files with 74 additions and 29 deletions
+10 -8
View File
@@ -34,25 +34,27 @@ A Telegram chat will be created once the bridge is stable enough.
You should be automatically invited into portal rooms for your groups and channels if you You should be automatically invited into portal rooms for your groups and channels if you
1. (re)start the bridge, 1. (re)start the bridge,
2. receive a messages in the chat or 2. receive a messages in the chat or
3. ~~receive an invite to the chat~~ (not yet implemented) 3. receive an invite to the chat
Support for inviting users both Telegram and Matrix users to Telegram portal rooms is planned, but not yet implemented. Support for inviting users both Telegram and Matrix users to Telegram portal rooms is planned, but not yet implemented.
#### Private messaging #### Private messaging
**Initiating private chats is not yet implemented.** **Initiating private chats is not yet implemented.** In order to initiate a private chat,
send a message in either direction with another Telegram client.
You can start private chats by simply inviting the Matrix puppet of the Telegram user you want to chat with to a private room. ~~You can start private chats by simply inviting the Matrix puppet of the Telegram user you want to chat with to a private room.~~
If you don't know the MXID of the puppet, you can search for users using the `search <query>` management command. ~~If you don't know the MXID of the puppet, you can search for users using the `search <query>` management command.~~
#### Bot commands #### Bot commands
**Initiating private chats is not yet implemented.** **Initiating private chats is not yet implemented.** In order to initiate a chat with a,
bot, send a message to the bot with another Telegram client.
Initiating chats with bots is no different from initiating chats with real Telegram users. ~~Initiating chats with bots is no different from initiating chats with real Telegram users.~~
The bridge translates `!commands` into `/commands`, which allows you to use Telegram bots without constantly escaping ~~The bridge translates `!commands` into `/commands`, which allows you to use Telegram bots without constantly escaping
the slash. Please note that when messaging a bot for the first time, it may expect you to run `!start` first. The bridge the slash. Please note that when messaging a bot for the first time, it may expect you to run `!start` first. The bridge
does not do this automatically. does not do this automatically.~~
## Features & Roadmap ## Features & Roadmap
* Matrix → Telegram * Matrix → Telegram
+38 -1
View File
@@ -24,6 +24,41 @@ from contextlib import contextmanager
from .intent_api import HTTPAPI from .intent_api import HTTPAPI
class StateStore:
def __init__(self):
self.memberships = {}
self.power_levels = {}
self.power_level_requirements = {}
def _get_membership(self, room, user):
return self.memberships.get(room, {}).get(user, "left")
def is_joined(self, room, user):
return self._get_membership(room, user) == "join"
def _set_membership(self, room, user, membership):
if room not in self.memberships:
self.memberships[room] = {}
self.memberships[room][user] = membership
def joined(self, room, user):
return self._set_membership(room, user, "join")
def invited(self, room, user):
return self._set_membership(room, user, "invited")
def left(self, room, user):
return self._set_membership(room, user, "left")
def has_power_level(self, room, user, event):
return True
def set_power_level(self, room, user, level):
if not room in self.power_levels:
self.power_levels[room] = {}
self.power_levels[room][user] = level
class AppService: class AppService:
def __init__(self, server, domain, as_token, hs_token, bot_localpart, loop=None, log=None, def __init__(self, server, domain, as_token, hs_token, bot_localpart, loop=None, log=None,
query_user=None, query_alias=None): query_user=None, query_alias=None):
@@ -32,6 +67,7 @@ class AppService:
self.as_token = as_token self.as_token = as_token
self.hs_token = hs_token self.hs_token = hs_token
self.bot_mxid = f"@{bot_localpart}:{domain}" self.bot_mxid = f"@{bot_localpart}:{domain}"
self.state_store = StateStore()
self.transactions = [] self.transactions = []
@@ -71,7 +107,8 @@ class AppService:
@contextmanager @contextmanager
def run(self, host="127.0.0.1", port=8080): def run(self, host="127.0.0.1", port=8080):
self._http_session = aiohttp.ClientSession(loop=self.loop) self._http_session = aiohttp.ClientSession(loop=self.loop)
self._intent = HTTPAPI(base_url=self.server, bot_mxid=self.bot_mxid, token=self.as_token, log=self.log).bot_intent() self._intent = HTTPAPI(base_url=self.server, bot_mxid=self.bot_mxid, token=self.as_token,
log=self.log, state_store=self.state_store).bot_intent()
yield partial(aiohttp.web.run_app, self.app, host=host, port=port) yield partial(aiohttp.web.run_app, self.app, host=host, port=port)
+26 -20
View File
@@ -22,14 +22,17 @@ from matrix_client.errors import MatrixRequestError
class HTTPAPI(MatrixHttpApi): class HTTPAPI(MatrixHttpApi):
def __init__(self, base_url, bot_mxid=None, token=None, identity=None, log=None): def __init__(self, base_url, bot_mxid=None, token=None, identity=None, log=None,
state_store=None):
self.base_url = base_url self.base_url = base_url
self.token = token self.token = token
self.identity = identity self.identity = identity
self.txn_id = 0 self.txn_id = 0
self.bot_mxid = bot_mxid self.bot_mxid = bot_mxid
self.log = log self.intent_log = log.getChild("intent")
self.log = log.getChild("api")
self.validate_cert = True self.validate_cert = True
self.state_store = state_store
self.children = {} self.children = {}
def user(self, user): def user(self, user):
@@ -41,10 +44,10 @@ class HTTPAPI(MatrixHttpApi):
return child return child
def bot_intent(self): def bot_intent(self):
return IntentAPI(self.bot_mxid, self, log=self.log) return IntentAPI(self.bot_mxid, self, state_store=self.state_store, log=self.intent_log)
def intent(self, user): def intent(self, user):
return IntentAPI(user, self.user(user), self, log=self.log) return IntentAPI(user, self.user(user), self, self.state_store, self.intent_log)
def _send(self, method, path, content=None, query_params={}, headers={}, def _send(self, method, path, content=None, query_params={}, headers={},
api_path="/_matrix/client/r0"): api_path="/_matrix/client/r0"):
@@ -132,7 +135,7 @@ def matrix_error_code(err):
class IntentAPI: class IntentAPI:
mxid_regex = re.compile("@(.+):(.+)") mxid_regex = re.compile("@(.+):(.+)")
def __init__(self, mxid, client, bot=None, log=None): def __init__(self, mxid, client, bot=None, state_store=None, log=None):
self.client = client self.client = client
self.bot = bot self.bot = bot
self.mxid = mxid self.mxid = mxid
@@ -143,15 +146,15 @@ class IntentAPI:
raise ValueError("invalid MXID") raise ValueError("invalid MXID")
self.localpart = results.group(1) self.localpart = results.group(1)
self.memberships = {} self.state_store = state_store
self.power_levels = {}
self.registered = False self.registered = False
def user(self, user): def user(self, user):
if not self.bot: if not self.bot:
return self.client.intent(user) return self.client.intent(user)
else: else:
raise ValueError("IntentAPI#user() is only available for base intent objects.") self.log.warning("Called IntentAPI#user() of child intent object.")
return self.bot.intent(user)
# region User actions # region User actions
@@ -189,7 +192,9 @@ class IntentAPI:
def invite(self, room_id, user_id): def invite(self, room_id, user_id):
self._ensure_joined(room_id) self._ensure_joined(room_id)
try: try:
return self.client.invite_user(room_id, user_id) response = self.client.invite_user(room_id, user_id)
self.state_store.set_invited(room_id, user_id)
return response
except MatrixRequestError as e: except MatrixRequestError as e:
if matrix_error_code(e) != "M_FORBIDDEN": if matrix_error_code(e) != "M_FORBIDDEN":
raise IntentError(f"Failed to invite {user_id} to {room_id}", e) raise IntentError(f"Failed to invite {user_id} to {room_id}", e)
@@ -212,10 +217,10 @@ class IntentAPI:
return self.client.set_typing(room_id, is_typing, timeout) return self.client.set_typing(room_id, is_typing, timeout)
def send_notice(self, room_id, text, html=None): def send_notice(self, room_id, text, html=None):
self.send_text(room_id, text, html, "m.notice") return self.send_text(room_id, text, html, "m.notice")
def send_emote(self, room_id, text, html=None): def send_emote(self, room_id, text, html=None):
self.send_text(room_id, text, html, "m.emote") return self.send_text(room_id, text, html, "m.emote")
def send_image(self, room_id, url, info={}, text=None): def send_image(self, room_id, url, info={}, text=None):
return self.send_file(room_id, url, info, text, "m.image") return self.send_file(room_id, url, info, text, "m.image")
@@ -249,21 +254,21 @@ class IntentAPI:
self._ensure_joined(room_id) self._ensure_joined(room_id)
self.client.kick_user(room_id, user_id, message) self.client.kick_user(room_id, user_id, message)
def send_event(self, room_id, type, body, txn_id=None, timestamp=None): def send_event(self, room_id, type, body, txn_id=None):
self._ensure_joined(room_id) self._ensure_joined(room_id)
self._ensure_has_power_level_for(room_id, type) self._ensure_has_power_level_for(room_id, type)
return self.client.send_message_event(room_id, type, body, txn_id, timestamp) return self.client.send_message_event(room_id, type, body, txn_id)
def send_state_event(self, room_id, type, body, state_key="", timestamp=None): def send_state_event(self, room_id, type, body, state_key=""):
self._ensure_joined(room_id) self._ensure_joined(room_id)
self._ensure_has_power_level_for(room_id, type) self._ensure_has_power_level_for(room_id, type)
return self.client.send_state_event(room_id, type, body, state_key, timestamp) return self.client.send_state_event(room_id, type, body, state_key)
def join_room(self, room_id): def join_room(self, room_id):
return self._ensure_joined(room_id, ignore_cache=True) return self._ensure_joined(room_id, ignore_cache=True)
def leave_room(self, room_id): def leave_room(self, room_id):
self.memberships[room_id] = "left" self.state_store.left(room_id, self.mxid)
return self.client.leave_room(room_id) return self.client.leave_room(room_id)
def get_room_members(self, room_id): def get_room_members(self, room_id):
@@ -278,19 +283,19 @@ class IntentAPI:
# region Ensure functions # region Ensure functions
def _ensure_joined(self, room_id, ignore_cache=False): def _ensure_joined(self, room_id, ignore_cache=False):
if not ignore_cache and self.memberships.get(room_id, "") == "join": if not ignore_cache and self.state_store.is_joined(room_id, self.mxid):
return return
self._ensure_registered() self._ensure_registered()
try: try:
self.client.join_room(room_id) self.client.join_room(room_id)
self.memberships[room_id] = "join" self.state_store.joined(room_id, self.mxid)
except MatrixRequestError as e: except MatrixRequestError as e:
if matrix_error_code(e) != "M_FORBIDDEN" and not self.bot: if matrix_error_code(e) != "M_FORBIDDEN" and not self.bot:
raise IntentError(f"Failed to join room {room_id} as {self.mxid}", e) raise IntentError(f"Failed to join room {room_id} as {self.mxid}", e)
try: try:
self.bot.invite_user(room_id, self.mxid) self.bot.invite_user(room_id, self.mxid)
self.client.join_room(room_id) self.client.join_room(room_id)
self.memberships[room_id] = "join" self.state_store.joined(room_id, self.mxid)
except MatrixRequestError as e2: except MatrixRequestError as e2:
raise IntentError(f"Failed to join room {room_id} as {self.mxid}", e2) raise IntentError(f"Failed to join room {room_id} as {self.mxid}", e2)
@@ -306,7 +311,8 @@ class IntentAPI:
self.registered = True self.registered = True
def _ensure_has_power_level_for(self, room_id, event_type): def _ensure_has_power_level_for(self, room_id, event_type):
if self.state_store.has_power_level(room_id, self.mxid, event_type):
return
# TODO implement # TODO implement
pass
# endregion # endregion