Jelajahi Sumber

Add untested provisioning API

Tulir Asokan 4 tahun lalu
induk
melakukan
82af16ef33

+ 1 - 0
mausignald/types.py

@@ -94,6 +94,7 @@ class Profile(SerializableAttrs['Profile']):
     identity_key: str = ""
     unidentified_access: str = ""
     unrestricted_unidentified_access: bool = False
+    address: Optional[Address] = None
 
 
 @dataclass

+ 3 - 3
mautrix_signal/__main__.py

@@ -60,9 +60,9 @@ class SignalBridge(Bridge):
     def prepare_bridge(self) -> None:
         self.signal = SignalHandler(self)
         super().prepare_bridge()
-        cfg = self.config["appservice.provisioning"]
-        # self.provisioning_api = ProvisioningAPI(cfg["shared_secret"])
-        # self.az.app.add_subapp(cfg["prefix"], self.provisioning_api.app)
+        cfg = self.config["bridge.provisioning"]
+        self.provisioning_api = ProvisioningAPI(self, cfg["shared_secret"])
+        self.az.app.add_subapp(cfg["prefix"], self.provisioning_api.app)
 
     async def start(self) -> None:
         await self.db.start()

+ 6 - 6
mautrix_signal/config.py

@@ -45,12 +45,6 @@ class Config(BaseBridgeConfig):
 
         copy("homeserver.asmux")
 
-        copy("appservice.provisioning.enabled")
-        copy("appservice.provisioning.prefix")
-        copy("appservice.provisioning.shared_secret")
-        if base["appservice.provisioning.shared_secret"] == "generate":
-            base["appservice.provisioning.shared_secret"] = self._new_token()
-
         copy("appservice.community_id")
 
         copy("signal.socket_path")
@@ -89,6 +83,12 @@ class Config(BaseBridgeConfig):
         copy("bridge.delivery_error_reports")
         copy("bridge.resend_bridge_info")
 
+        copy("bridge.provisioning.enabled")
+        copy("bridge.provisioning.prefix")
+        copy("bridge.provisioning.shared_secret")
+        if base["bridge.provisioning.shared_secret"] == "generate":
+            base["bridge.provisioning.shared_secret"] = self._new_token()
+
         copy("bridge.command_prefix")
 
         copy_dict("bridge.permissions")

+ 11 - 11
mautrix_signal/example-config.yaml

@@ -28,17 +28,6 @@ appservice:
     # The full URI to the database. Only Postgres is currently supported.
     database: postgres://username:password@hostname/db
 
-    # Provisioning API part of the web server for automated portal creation and fetching information.
-    # Used by things like mautrix-manager (https://github.com/tulir/mautrix-manager).
-    provisioning:
-        # Whether or not the provisioning API should be enabled.
-        enabled: true
-        # The prefix to use in the provisioning API endpoints.
-        prefix: /_matrix/provision/v1
-        # The shared secret to authorize users of the API.
-        # Set to "generate" to generate and save a new token.
-        shared_secret: generate
-
     # The unique ID of this appservice.
     id: signal
     # Username of the appservice bot.
@@ -164,6 +153,17 @@ bridge:
     # except if the config file is not writable.
     resend_bridge_info: false
 
+    # Provisioning API part of the web server for automated portal creation and fetching information.
+    # Used by things like mautrix-manager (https://github.com/tulir/mautrix-manager).
+    provisioning:
+        # Whether or not the provisioning API should be enabled.
+        enabled: true
+        # The prefix to use in the provisioning API endpoints.
+        prefix: /_matrix/provision/v1
+        # The shared secret to authorize users of the API.
+        # Set to "generate" to generate and save a new token.
+        shared_secret: generate
+
     # The prefix for commands. Only required in non-management rooms.
     command_prefix: "!signal"
 

+ 69 - 29
mautrix_signal/web/provisioning_api.py

@@ -13,29 +13,44 @@
 #
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
-from typing import Awaitable, Dict
+from typing import Awaitable, Dict, TYPE_CHECKING
 import logging
+import asyncio
 import json
 
 from aiohttp import web
 
+from mausignald.types import Address, Account
+from mausignald.errors import LinkingTimeout
 from mautrix.types import UserID
 from mautrix.util.logging import TraceLogger
 
 from .. import user as u
 
+if TYPE_CHECKING:
+    from ..__main__ import SignalBridge
+
 
 class ProvisioningAPI:
     log: TraceLogger = logging.getLogger("mau.web.provisioning")
     app: web.Application
+    bridge: 'SignalBridge'
 
-    def __init__(self, shared_secret: str) -> None:
+    def __init__(self, bridge: 'SignalBridge', shared_secret: str) -> None:
+        self.bridge = bridge
         self.app = web.Application()
         self.shared_secret = shared_secret
         self.app.router.add_get("/api/whoami", self.status)
-        self.app.router.add_options("/api/login", self.login_options)
-        self.app.router.add_post("/api/login", self.login)
-        self.app.router.add_post("/api/logout", self.logout)
+        self.app.router.add_options("/api/link", self.login_options)
+        self.app.router.add_options("/api/link/wait", self.login_options)
+        # self.app.router.add_options("/api/register", self.login_options)
+        # self.app.router.add_options("/api/register/code", self.login_options)
+        # self.app.router.add_options("/api/logout", self.login_options)
+        self.app.router.add_post("/api/link", self.link)
+        self.app.router.add_post("/api/link/wait", self.link_wait)
+        # self.app.router.add_post("/api/register", self.register)
+        # self.app.router.add_post("/api/register/code", self.register_code)
+        # self.app.router.add_post("/api/logout", self.logout)
 
     @property
     def _acao_headers(self) -> Dict[str, str]:
@@ -60,17 +75,17 @@ class ProvisioningAPI:
             token = request.headers["Authorization"]
             token = token[len("Bearer "):]
         except KeyError:
-            raise web.HTTPBadRequest(body='{"error": "Missing Authorization header"}',
+            raise web.HTTPBadRequest(text='{"error": "Missing Authorization header"}',
                                      headers=self._headers)
         except IndexError:
-            raise web.HTTPBadRequest(body='{"error": "Malformed Authorization header"}',
+            raise web.HTTPBadRequest(text='{"error": "Malformed Authorization header"}',
                                      headers=self._headers)
         if token != self.shared_secret:
-            raise web.HTTPForbidden(body='{"error": "Invalid token"}', headers=self._headers)
+            raise web.HTTPForbidden(text='{"error": "Invalid token"}', headers=self._headers)
         try:
             user_id = request.query["user_id"]
         except KeyError:
-            raise web.HTTPBadRequest(body='{"error": "Missing user_id query param"}',
+            raise web.HTTPBadRequest(text='{"error": "Missing user_id query param"}',
                                      headers=self._headers)
 
         return u.User.get_by_mxid(UserID(user_id))
@@ -80,35 +95,60 @@ class ProvisioningAPI:
         data = {
             "permissions": user.permission_level,
             "mxid": user.mxid,
-            "twitter": None,
+            "signal": None,
         }
         if await user.is_logged_in():
-            data["twitter"] = (await user.get_info()).serialize()
+            profile = await self.bridge.signal.get_profile(username=user.username,
+                                                           address=Address(number=user.username))
+            data["signal"] = {
+                "number": profile.address.number or user.username,
+                "uuid": profile.address.uuid or user.uuid,
+                "name": profile.name
+            }
         return web.json_response(data, headers=self._acao_headers)
 
-    async def login(self, request: web.Request) -> web.Response:
+    async def link(self, request: web.Request) -> web.Response:
         user = await self.check_token(request)
 
         try:
             data = await request.json()
         except json.JSONDecodeError:
-            raise web.HTTPBadRequest(body='{"error": "Malformed JSON"}', headers=self._headers)
+            raise web.HTTPBadRequest(text='{"error": "Malformed JSON"}', headers=self._headers)
 
-        try:
-            auth_token = data["auth_token"]
-            csrf_token = data["csrf_token"]
-        except KeyError:
-            raise web.HTTPBadRequest(body='{"error": "Missing keys"}', headers=self._headers)
+        device_name = data.get("device_name", "Mautrix-Signal bridge")
+        uri_future = asyncio.Future()
 
-        try:
-            await user.connect(auth_token=auth_token, csrf_token=csrf_token)
-        except Exception:
-            self.log.debug("Failed to log in", exc_info=True)
-            raise web.HTTPUnauthorized(body='{"error": "Twitter authorization failed"}',
-                                       headers=self._headers)
-        return web.Response(body='{}', status=200, headers=self._headers)
-
-    async def logout(self, request: web.Request) -> web.Response:
+        async def _callback(uri: str) -> None:
+            uri_future.set_result(uri)
+
+        async def _link() -> Account:
+            account = await self.bridge.signal.link(_callback, device_name=device_name)
+            await user.on_signin(account)
+            return account
+
+        user.command_status = {
+            "action": "Link",
+            "task": self.bridge.loop.create_task(_link()),
+        }
+
+        return web.json_response({"uri": await uri_future}, headers=self._acao_headers)
+
+    async def link_wait(self, request: web.Request) -> web.Response:
         user = await self.check_token(request)
-        await user.logout()
-        return web.json_response({}, headers=self._acao_headers)
+        if not user.command_status or user.command_status["action"] != "Link":
+            raise web.HTTPBadRequest(text='{"error": "No Signal linking started"}',
+                                     headers=self._headers)
+        try:
+            account = await user.command_status["task"]
+        except LinkingTimeout:
+            raise web.HTTPBadRequest(text='{"error": "Signal linking timed out"}',
+                                     headers=self._headers)
+        return web.json_response({
+            "number": account.username,
+            "uuid": account.uuid,
+        })
+
+    # async def logout(self, request: web.Request) -> web.Response:
+    #     user = await self.check_token(request)
+    #     await user.()
+    #     return web.json_response({}, headers=self._acao_headers)