Implement /portal/{mxid}/create

This commit is contained in:
Tulir Asokan
2018-07-14 23:14:04 +03:00
parent 34cc810d62
commit 4cef2be0db
6 changed files with 205 additions and 47 deletions
+1 -1
View File
@@ -93,7 +93,7 @@ if config["appservice.public.enabled"]:
appserv.app.add_subapp(config["appservice.public.prefix"] or "/public", public_website.app) appserv.app.add_subapp(config["appservice.public.prefix"] or "/public", public_website.app)
if config["appservice.provisioning.enabled"]: if config["appservice.provisioning.enabled"]:
provisioning_api = ProvisioningAPI(config, loop) provisioning_api = ProvisioningAPI(config, appserv, loop)
appserv.app.add_subapp(config["appservice.provisioning.prefix"] or "/_matrix/provisioning", appserv.app.add_subapp(config["appservice.provisioning.prefix"] or "/_matrix/provisioning",
provisioning_api.app) provisioning_api.app)
+15 -11
View File
@@ -19,10 +19,11 @@ import asyncio
from telethon.errors import * from telethon.errors import *
from telethon.tl.types import ChatForbidden, ChannelForbidden from telethon.tl.types import ChatForbidden, ChannelForbidden
from mautrix_appservice import MatrixRequestError from mautrix_appservice import MatrixRequestError, IntentAPI
from .. import portal as po, user as u from .. import portal as po, user as u
from . import command_handler, CommandEvent, SECTION_ADMIN, SECTION_CREATING_PORTALS, SECTION_PORTAL_MANAGEMENT from . import (command_handler, CommandEvent,
SECTION_ADMIN, SECTION_CREATING_PORTALS, SECTION_PORTAL_MANAGEMENT)
@command_handler(needs_admin=True, needs_auth=False, name="set-pl", @command_handler(needs_admin=True, needs_auth=False, name="set-pl",
@@ -65,7 +66,7 @@ async def invite_link(evt: CommandEvent):
return await evt.reply("You don't have the permission to create an invite link.") return await evt.reply("You don't have the permission to create an invite link.")
async def _has_access_to(room: str, intent, sender: u.User, event: str, default: int = 50): async def user_has_power_level(room: str, intent, sender: u.User, event: str, default: int = 50):
if sender.is_admin: if sender.is_admin:
return True return True
# Make sure the state store contains the power levels. # Make sure the state store contains the power levels.
@@ -87,7 +88,7 @@ async def _get_portal_and_check_permission(evt: CommandEvent, permission: str,
that_this = "This" if room_id == evt.room_id else "That" that_this = "This" if room_id == evt.room_id else "That"
return await evt.reply(f"{that_this} is not a portal room."), False return await evt.reply(f"{that_this} is not a portal room."), False
if not await _has_access_to(portal.mxid, evt.az.intent, evt.sender, permission): if not await user_has_power_level(portal.mxid, evt.az.intent, evt.sender, permission):
action = action or f"{permission.replace('_', ' ')}s" action = action or f"{permission.replace('_', ' ')}s"
return await evt.reply(f"You do not have the permissions to {action} that portal."), False return await evt.reply(f"You do not have the permissions to {action} that portal."), False
return portal, True return portal, True
@@ -166,8 +167,8 @@ async def bridge(evt: CommandEvent):
if portal: if portal:
return await evt.reply(f"{that_this} room is already a portal room.") return await evt.reply(f"{that_this} room is already a portal room.")
if not await _has_access_to(room_id, evt.az.intent, evt.sender, "bridge"): if not await user_has_power_level(room_id, evt.az.intent, evt.sender, "bridge"):
return await evt.reply("You do not have the permissions to bridge that room.") return await evt.reply(f"You do not have the permissions to bridge {that_this} room.")
# The /id bot command provides the prefixed ID, so we assume # The /id bot command provides the prefixed ID, so we assume
tgid = evt.args[0] tgid = evt.args[0]
@@ -192,7 +193,7 @@ async def bridge(evt: CommandEvent):
has_portal_message = ( has_portal_message = (
"That Telegram chat already has a portal at " "That Telegram chat already has a portal at "
f"[{portal.alias or portal.mxid}](https://matrix.to/#/{portal.mxid}). ") f"[{portal.alias or portal.mxid}](https://matrix.to/#/{portal.mxid}). ")
if not await _has_access_to(portal.mxid, evt.az.intent, evt.sender, "unbridge"): if not await user_has_power_level(portal.mxid, evt.az.intent, evt.sender, "unbridge"):
return await evt.reply(f"{has_portal_message}" return await evt.reply(f"{has_portal_message}"
"Additionally, you do not have the permissions to unbridge " "Additionally, you do not have the permissions to unbridge "
"that room.") "that room.")
@@ -291,7 +292,7 @@ async def confirm_bridge(evt: CommandEvent):
direct = False direct = False
portal.mxid = bridge_to_mxid portal.mxid = bridge_to_mxid
portal.title, portal.about, levels = await _get_initial_state(evt) portal.title, portal.about, levels = await get_initial_state(evt.az.intent, evt.room_id)
portal.photo_id = "" portal.photo_id = ""
portal.save() portal.save()
@@ -301,8 +302,8 @@ async def confirm_bridge(evt: CommandEvent):
return await evt.reply("Bridging complete. Portal synchronization should begin momentarily.") return await evt.reply("Bridging complete. Portal synchronization should begin momentarily.")
async def _get_initial_state(evt: CommandEvent): async def get_initial_state(intent: IntentAPI, room_id: str):
state = await evt.az.intent.get_room_state(evt.room_id) state = await intent.get_room_state(room_id)
title = None title = None
about = None about = None
levels = None levels = None
@@ -336,7 +337,10 @@ async def create(evt: CommandEvent):
if po.Portal.get_by_mxid(evt.room_id): if po.Portal.get_by_mxid(evt.room_id):
return await evt.reply("This is already a portal room.") return await evt.reply("This is already a portal room.")
title, about, levels = await _get_initial_state(evt) if not await user_has_power_level(evt.room_id, evt.az.intent, evt.sender, "bridge"):
return await evt.reply("You do not have the permissions to bridge this room.")
title, about, levels = await get_initial_state(evt.az.intent, evt.room_id)
if not title: if not title:
return await evt.reply("Please set a title before creating a Telegram chat.") return await evt.reply("Please set a title before creating a Telegram chat.")
+3
View File
@@ -310,6 +310,9 @@ class User(AbstractUser):
@classmethod @classmethod
def get_by_mxid(cls, mxid, create=True): def get_by_mxid(cls, mxid, create=True):
if not mxid:
raise ValueError("Matrix ID can't be empty")
try: try:
return cls.by_mxid[mxid] return cls.by_mxid[mxid]
except KeyError: except KeyError:
+5 -5
View File
@@ -21,8 +21,8 @@ import logging
from telethon.errors import * from telethon.errors import *
from mautrix_telegram.commands.auth import enter_password from ...commands.auth import enter_password
from mautrix_telegram.util import format_duration from ...util import format_duration
class AuthAPI(abc.ABC): class AuthAPI(abc.ABC):
@@ -70,7 +70,7 @@ class AuthAPI(abc.ABC):
except Exception: except Exception:
self.log.exception("Error requesting phone code") self.log.exception("Error requesting phone code")
return self.get_login_response(mxid=user.mxid, state="request", status=500, return self.get_login_response(mxid=user.mxid, state="request", status=500,
errcode="exception", errcode="unknown_error",
error="Internal server error while requesting code.") error="Internal server error while requesting code.")
async def post_login_token(self, user, token): async def post_login_token(self, user, token):
@@ -124,7 +124,7 @@ class AuthAPI(abc.ABC):
except Exception: except Exception:
self.log.exception("Error sending phone code") self.log.exception("Error sending phone code")
return self.get_login_response(mxid=user.mxid, state="code", status=500, return self.get_login_response(mxid=user.mxid, state="code", status=500,
errcode="exception", errcode="unknown_error",
error="Internal server error while sending code.") error="Internal server error while sending code.")
async def post_login_password(self, user, password): async def post_login_password(self, user, password):
@@ -146,5 +146,5 @@ class AuthAPI(abc.ABC):
except Exception: except Exception:
self.log.exception("Error sending password") self.log.exception("Error sending password")
return self.get_login_response(mxid=user.mxid, state="password", status=500, return self.get_login_response(mxid=user.mxid, state="password", status=500,
errcode="exception", errcode="unknown_error",
error="Internal server error while sending password.") error="Internal server error while sending password.")
+79 -6
View File
@@ -16,32 +16,37 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from aiohttp import web from aiohttp import web
from typing import Tuple, Optional, Callable, Awaitable from typing import Tuple, Optional, Callable, Awaitable
import asyncio
import logging import logging
import json import json
from telethon.utils import get_peer_id, resolve_id from telethon.utils import get_peer_id, resolve_id
from mautrix_appservice import AppService, MatrixRequestError, IntentError
from ...user import User from ...user import User
from ...portal import Portal from ...portal import Portal
from ...commands.portal import user_has_power_level, get_initial_state
from ...config import Config
from ..common import AuthAPI from ..common import AuthAPI
class ProvisioningAPI(AuthAPI): class ProvisioningAPI(AuthAPI):
log = logging.getLogger("mau.web.provisioning") log = logging.getLogger("mau.web.provisioning")
def __init__(self, config, loop): def __init__(self, config: Config, az: AppService, loop: asyncio.AbstractEventLoop):
super().__init__(loop) super().__init__(loop)
self.secret = config["appservice.provisioning.shared_secret"] self.secret = config["appservice.provisioning.shared_secret"]
self.az = az
self.app = web.Application(loop=loop, middlewares=[self.error_middleware]) self.app = web.Application(loop=loop, middlewares=[self.error_middleware])
portal_prefix = "/portal/{mxid:![^/]+}" portal_prefix = "/portal/{mxid:![^/]+}"
self.app.router.add_route("GET", f"{portal_prefix}", self.get_portal_by_mxid) self.app.router.add_route("GET", f"{portal_prefix}", self.get_portal_by_mxid)
self.app.router.add_route("GET", "/portal/{tgid:-[0-9]+}", self.get_portal_by_tgid) self.app.router.add_route("GET", "/portal/{tgid:-[0-9]+}", self.get_portal_by_tgid)
# self.app.router.add_route("POST", portal_prefix + "/connect/{chat_id:[0-9]+}", self.app.router.add_route("POST", portal_prefix + "/connect/{chat_id:[0-9]+}",
# self.connect_chat) self.connect_chat)
# self.app.router.add_route("POST", f"{portal_prefix}/create", self.create_chat) self.app.router.add_route("POST", f"{portal_prefix}/create", self.create_chat)
# self.app.router.add_route("POST", f"{portal_prefix}/disconnect", self.disconnect_chat) self.app.router.add_route("POST", f"{portal_prefix}/disconnect", self.disconnect_chat)
user_prefix = "/user/{mxid:@[^:]*:[^/]+}" user_prefix = "/user/{mxid:@[^:]*:[^/]+}"
self.app.router.add_route("GET", f"{user_prefix}", self.get_user_info) self.app.router.add_route("GET", f"{user_prefix}", self.get_user_info)
@@ -88,6 +93,69 @@ class ProvisioningAPI(AuthAPI):
"megagroup": portal.megagroup, "megagroup": portal.megagroup,
}) })
async def connect_chat(self, request: web.Request) -> web.Response:
return web.Response(status=501)
async def create_chat(self, request: web.Request) -> web.Response:
data = await self.get_data(request)
if not data:
return self.get_error_response(400, "json_invalid", "Invalid JSON.")
room_id = request.match_info["mxid"]
if Portal.get_by_mxid(room_id):
return self.get_error_response(409, "room_already_bridged",
"Room is already bridged to another Telegram chat.")
user, err = await self.get_user(request.query.get("user_id", None), expect_logged_in=None,
require_puppeting=False)
if err is not None:
return err
elif not await user.is_logged_in() or user.is_bot:
return self.get_error_response(403, "not_logged_in_real_account",
"You are not logged in with a real account.")
elif not await user_has_power_level(room_id, self.az.intent, user, "bridge"):
return self.get_error_response(403, "not_enough_permissions",
"You do not have the permissions to bridge that room.")
try:
title, about, _ = await get_initial_state(self.az.intent, room_id)
except (MatrixRequestError, IntentError):
return self.get_error_response(403, "bot_not_in_room",
"The bridge bot is not in the given room.")
about = data.get("about", about)
title = data.get("title", title)
if len(title) == 0:
return self.get_error_response(400, "body_value_invalid", "Title can not be empty.")
type = data.get("type", "")
if type not in ("group", "chat", "supergroup", "channel"):
return self.get_error_response(400, "body_value_invalid",
"Given chat type is not valid.")
supergroup = type == "supergroup"
type = {
"supergroup": "channel",
"channel": "channel",
"chat": "chat",
"group": "chat",
}[type]
portal = Portal(tgid=None, mxid=room_id, title=title, about=about, peer_type=type)
try:
await portal.create_telegram_chat(user, supergroup=supergroup)
except ValueError as e:
portal.delete()
return self.get_error_response(500, "unknown_error", e.args[0])
return web.json_response({
"chat_id": portal.tgid,
})
async def disconnect_chat(self, request: web.Request) -> web.Response:
return web.Response(status=501)
async def get_user_info(self, request: web.Request) -> web.Response: async def get_user_info(self, request: web.Request) -> web.Response:
data, user, err = await self.get_user_request_info(request, expect_logged_in=None, data, user, err = await self.get_user_request_info(request, expect_logged_in=None,
require_puppeting=False) require_puppeting=False)
@@ -187,10 +255,11 @@ class ProvisioningAPI(AuthAPI):
} }
else: else:
resp = { resp = {
"state": state,
"error": error, "error": error,
"errcode": errcode, "errcode": errcode,
} }
if state:
resp["state"] = state
return web.json_response(resp, status=status) return web.json_response(resp, status=status)
def check_authorization(self, request: web.Request) -> bool: def check_authorization(self, request: web.Request) -> bool:
@@ -206,6 +275,10 @@ class ProvisioningAPI(AuthAPI):
async def get_user(self, mxid: str, expect_logged_in: Optional[bool] = False, async def get_user(self, mxid: str, expect_logged_in: Optional[bool] = False,
require_puppeting: bool = True, require_puppeting: bool = True,
) -> Tuple[Optional[User], Optional[web.Response]]: ) -> Tuple[Optional[User], Optional[web.Response]]:
if not mxid:
return None, self.get_login_response(error="User ID not given.",
errcode="mxid_empty", status=400)
user = await User.get_by_mxid(mxid).ensure_started(even_if_no_session=True) user = await User.get_by_mxid(mxid).ensure_started(even_if_no_session=True)
if require_puppeting and not user.puppet_whitelisted: if require_puppeting and not user.puppet_whitelisted:
return user, self.get_login_response(error="You are not whitelisted.", return user, self.get_login_response(error="You are not whitelisted.",
+102 -24
View File
@@ -35,7 +35,7 @@ paths:
schema: schema:
$ref: "#/definitions/PortalInfo" $ref: "#/definitions/PortalInfo"
400: 400:
$ref: "#/responses/MissingMXIDError" $ref: "#/responses/BadRequest"
404: 404:
description: Unknown portal description: Unknown portal
schema: schema:
@@ -127,9 +127,16 @@ paths:
enum: enum:
- delete - delete
- unbridge - unbridge
- name: user_id
in: query
description: Optional Matrix user ID to check if the user has permissions to do the bridging.
required: false
type: string
responses: responses:
400: 400:
$ref: "#/responses/MissingMXIDError" $ref: "#/responses/BadRequest"
401:
$ref: "#/responses/PermissionError"
409: 409:
description: Matrix room or Telegram chat is already bridged description: Matrix room or Telegram chat is already bridged
schema: schema:
@@ -152,8 +159,33 @@ paths:
summary: Create a new Telegram chat for the given room summary: Create a new Telegram chat for the given room
tags: [Bridging] tags: [Bridging]
responses: responses:
200:
description: Telegram chat created
schema:
type: object
properties:
chat_id:
type: integer
400: 400:
$ref: "#/responses/MissingMXIDError" $ref: "#/responses/BadRequest"
401:
$ref: "#/responses/PermissionError"
403:
description: "Given user isn't logged in with a real account or doesn't have permission to bridge the room, or the bridge bot is not in the room"
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
enum:
- not_logged_in_real_account
- not_enough_permissions
- bot_not_in_room
error:
$ref: "#/definitions/HumanReadableError"
409: 409:
description: Room is already bridged description: Room is already bridged
schema: schema:
@@ -174,6 +206,34 @@ paths:
description: The Matrix ID of the room whose bridging status to get description: The Matrix ID of the room whose bridging status to get
required: true required: true
type: string type: string
- name: body
in: body
required: true
schema:
type: object
required: [type]
properties:
type:
description: The type of chat to create
type: string
example: supergroup
enum:
- chat
- supergroup
- channel
title:
description: Title for the new chat
type: string
example: Mautrix-Telegram Bridge
about:
description: About text for the new chat
type: string
example: Discussion about mautrix-telegram
- name: user_id
in: query
description: Matrix user to create the chat as.
required: true
type: string
/portal/{room_id}/disconnect: /portal/{room_id}/disconnect:
post: post:
operationId: disconnect_portal operationId: disconnect_portal
@@ -183,7 +243,9 @@ paths:
202: 202:
description: Room unbridging initiated description: Room unbridging initiated
400: 400:
$ref: "#/responses/MissingMXIDError" $ref: "#/responses/BadRequest"
401:
$ref: "#/responses/PermissionError"
404: 404:
description: Unknown portal description: Unknown portal
schema: schema:
@@ -204,6 +266,11 @@ paths:
description: The Matrix ID of the room whose bridging status to get description: The Matrix ID of the room whose bridging status to get
required: true required: true
type: string type: string
- name: user_id
in: query
description: Optional Matrix user ID to check if the user has permissions to do the bridging.
required: false
type: string
/user/{user_id}: /user/{user_id}:
get: get:
@@ -216,7 +283,7 @@ paths:
schema: schema:
$ref: "#/definitions/UserInfo" $ref: "#/definitions/UserInfo"
400: 400:
$ref: "#/responses/MissingMXIDError" $ref: "#/responses/BadRequest"
403: 403:
$ref: "#/responses/NotWhitelistedError" $ref: "#/responses/NotWhitelistedError"
500: 500:
@@ -238,7 +305,7 @@ paths:
schema: schema:
$ref: "#/definitions/UserChats" $ref: "#/definitions/UserChats"
400: 400:
$ref: "#/responses/MissingMXIDError" $ref: "#/responses/BadRequest"
403: 403:
description: User is not logged in or not whitelisted description: User is not logged in or not whitelisted
schema: schema:
@@ -274,7 +341,7 @@ paths:
schema: schema:
$ref: "#/definitions/AuthSuccess" $ref: "#/definitions/AuthSuccess"
400: 400:
$ref: "#/responses/MissingMXIDError" $ref: "#/responses/BadRequest"
401: 401:
description: Invalid or expired bot token or invalid shared secret description: Invalid or expired bot token or invalid shared secret
schema: schema:
@@ -325,7 +392,7 @@ paths:
schema: schema:
$ref: "#/definitions/AuthSuccess" $ref: "#/definitions/AuthSuccess"
400: 400:
description: Invalid phone number or JSON or missing Matrix ID description: Invalid phone number or JSON
schema: schema:
type: object type: object
title: Error title: Error
@@ -337,7 +404,6 @@ paths:
example: machine_readable_error example: machine_readable_error
enum: enum:
- phone_number_invalid - phone_number_invalid
- mxid_empty
- json_invalid - json_invalid
error: error:
$ref: "#/definitions/HumanReadableError" $ref: "#/definitions/HumanReadableError"
@@ -436,7 +502,7 @@ paths:
schema: schema:
$ref: "#/definitions/AuthSuccess" $ref: "#/definitions/AuthSuccess"
400: 400:
$ref: "#/responses/MissingMXIDError" $ref: "#/responses/BadRequest"
401: 401:
description: Invalid phone code or shared secret description: Invalid phone code or shared secret
schema: schema:
@@ -500,7 +566,7 @@ paths:
schema: schema:
$ref: "#/definitions/AuthSuccess" $ref: "#/definitions/AuthSuccess"
400: 400:
description: Missing password or Matrix ID or invalid JSON description: Missing password or invalid JSON
schema: schema:
type: object type: object
title: Error title: Error
@@ -512,7 +578,6 @@ paths:
example: <field>_empty example: <field>_empty
enum: enum:
- password_empty - password_empty
- mxid_empty
- json_invalid - json_invalid
error: error:
$ref: "#/definitions/HumanReadableError" $ref: "#/definitions/HumanReadableError"
@@ -582,8 +647,8 @@ responses:
username: username:
type: string type: string
description: The Telegram username the user is logged in as. description: The Telegram username the user is logged in as.
MissingMXIDError: BadRequest:
description: Missing Matrix ID or invalid JSON. description: Invalid JSON.
schema: schema:
type: object type: object
title: Error title: Error
@@ -593,8 +658,10 @@ responses:
title: Error code title: Error code
description: A machine-readable error code description: A machine-readable error code
enum: enum:
- mxid_empty
- json_invalid - json_invalid
- mxid_empty
- body_value_missing
- body_value_invalid
error: error:
$ref: "#/definitions/HumanReadableError" $ref: "#/definitions/HumanReadableError"
UnknownError: UnknownError:
@@ -608,23 +675,30 @@ responses:
title: Error code title: Error code
description: A machine-readable error code description: A machine-readable error code
enum: enum:
- exception - unknown_error
- unhandled_error
error: error:
type: string type: string
title: Error title: Error
description: A human-readable description of the error description: A human-readable description of the error
example: Internal server error while <action>. example: Internal server error while <action>.
PermissionError:
description: The given Matrix user doesn't have the permissions to do that.
schema:
type: object
title: Error
properties:
errcode:
type: string
title: Error code
description: A machine-readable error code
example: not_enough_permissions
enum: enum:
- Internal server error while requesting code. - not_enough_permissions
- Internal server error while sending code. error:
- Internal server error while sending password. $ref: "#/definitions/HumanReadableError"
- Internal server error while sending token.
definitions: definitions:
HumanReadableError:
type: string
description: A human-readable description of the error
example: A human-readable description of the error
UserInfo: UserInfo:
type: object type: object
properties: properties:
@@ -713,6 +787,10 @@ definitions:
type: string type: string
description: The Telegram username the user is logged in as. Only applicable if state=logged-in description: The Telegram username the user is logged in as. Only applicable if state=logged-in
HumanReadableError:
type: string
description: A human-readable description of the error
example: A human-readable description of the error
security: security:
- Bearer: [] - Bearer: []