|
| 1 | +# -*- coding: future_fstrings -*- |
| 2 | +# matrix-appservice-python - A Matrix Application Service framework written in Python. |
| 3 | +# Copyright (C) 2018 Tulir Asokan |
| 4 | +# |
| 5 | +# This program is free software: you can redistribute it and/or modify |
| 6 | +# it under the terms of the GNU General Public License as published by |
| 7 | +# the Free Software Foundation, either version 3 of the License, or |
| 8 | +# (at your option) any later version. |
| 9 | +# |
| 10 | +# This program is distributed in the hope that it will be useful, |
| 11 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 13 | +# GNU General Public License for more details. |
| 14 | +# |
| 15 | +# You should have received a copy of the GNU General Public License |
| 16 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 17 | +# |
| 18 | +# Partly based on github.com/Cadair/python-appservice-framework (MIT license) |
| 19 | +from contextlib import contextmanager |
| 20 | +from aiohttp import web |
| 21 | +import aiohttp |
| 22 | +import asyncio |
| 23 | +import logging |
| 24 | + |
| 25 | +from .intent_api import HTTPAPI |
| 26 | +from .state_store import StateStore |
| 27 | + |
| 28 | + |
| 29 | +class AppService: |
| 30 | + def __init__(self, server, domain, as_token, hs_token, bot_localpart, loop=None, log=None, |
| 31 | + verify_ssl=True, query_user=None, query_alias=None): |
| 32 | + self.server = server |
| 33 | + self.domain = domain |
| 34 | + self.verify_ssl = verify_ssl |
| 35 | + self.as_token = as_token |
| 36 | + self.hs_token = hs_token |
| 37 | + self.bot_mxid = f"@{bot_localpart}:{domain}" |
| 38 | + self.state_store = StateStore(autosave_file="mx-state.json") |
| 39 | + self.state_store.load("mx-state.json") |
| 40 | + |
| 41 | + self.transactions = [] |
| 42 | + |
| 43 | + self._http_session = None |
| 44 | + self._intent = None |
| 45 | + |
| 46 | + self.loop = loop or asyncio.get_event_loop() |
| 47 | + self.log = (logging.getLogger(log) if isinstance(log, str) |
| 48 | + else log or logging.getLogger("mautrix_appservice")) |
| 49 | + |
| 50 | + async def default_query_handler(_): |
| 51 | + return None |
| 52 | + |
| 53 | + self.query_user = query_user or default_query_handler |
| 54 | + self.query_alias = query_alias or default_query_handler |
| 55 | + |
| 56 | + self.event_handlers = [] |
| 57 | + |
| 58 | + self.app = web.Application(loop=self.loop) |
| 59 | + self.app.router.add_route("PUT", "/transactions/{transaction_id}", |
| 60 | + self._http_handle_transaction) |
| 61 | + self.app.router.add_route("GET", "/rooms/{alias}", self._http_query_alias) |
| 62 | + self.app.router.add_route("GET", "/users/{user_id}", self._http_query_user) |
| 63 | + |
| 64 | + self.matrix_event_handler(self.update_state_store) |
| 65 | + |
| 66 | + @property |
| 67 | + def http_session(self): |
| 68 | + if self._http_session is None: |
| 69 | + raise AttributeError("the http_session attribute can only be used " |
| 70 | + "from within the `AppService.run` context manager") |
| 71 | + else: |
| 72 | + return self._http_session |
| 73 | + |
| 74 | + @property |
| 75 | + def intent(self): |
| 76 | + if self._intent is None: |
| 77 | + raise AttributeError("the intent attribute can only be used from " |
| 78 | + "within the `AppService.run` context manager") |
| 79 | + else: |
| 80 | + return self._intent |
| 81 | + |
| 82 | + @contextmanager |
| 83 | + def run(self, host="127.0.0.1", port=8080): |
| 84 | + connector = None |
| 85 | + if self.server.startswith("https://") and not self.verify_ssl: |
| 86 | + connector = aiohttp.TCPConnector(verify_ssl=False) |
| 87 | + self._http_session = aiohttp.ClientSession(loop=self.loop, connector=connector) |
| 88 | + self._intent = HTTPAPI(base_url=self.server, domain=self.domain, bot_mxid=self.bot_mxid, |
| 89 | + token=self.as_token, log=self.log, state_store=self.state_store, |
| 90 | + client_session=self._http_session).bot_intent() |
| 91 | + |
| 92 | + yield self.loop.create_server(self.app.make_handler(), host, port) |
| 93 | + |
| 94 | + self._intent = None |
| 95 | + self._http_session.close() |
| 96 | + self._http_session = None |
| 97 | + |
| 98 | + def _check_token(self, request): |
| 99 | + try: |
| 100 | + token = request.rel_url.query["access_token"] |
| 101 | + except KeyError: |
| 102 | + return False |
| 103 | + |
| 104 | + if token != self.hs_token: |
| 105 | + return False |
| 106 | + |
| 107 | + return True |
| 108 | + |
| 109 | + async def _http_query_user(self, request): |
| 110 | + if not self._check_token(request): |
| 111 | + return web.Response(status=401) |
| 112 | + |
| 113 | + user_id = request.match_info["userId"] |
| 114 | + |
| 115 | + try: |
| 116 | + response = await self.query_user(user_id) |
| 117 | + except Exception: |
| 118 | + self.log.exception("Exception in user query handler") |
| 119 | + return web.Response(status=500) |
| 120 | + |
| 121 | + if not response: |
| 122 | + return web.Response(status=404) |
| 123 | + return web.json_response(response) |
| 124 | + |
| 125 | + async def _http_query_alias(self, request): |
| 126 | + if not self._check_token(request): |
| 127 | + return web.Response(status=401) |
| 128 | + |
| 129 | + alias = request.match_info["alias"] |
| 130 | + |
| 131 | + try: |
| 132 | + response = await self.query_alias(alias) |
| 133 | + except Exception: |
| 134 | + self.log.exception("Exception in alias query handler") |
| 135 | + return web.Response(status=500) |
| 136 | + |
| 137 | + if not response: |
| 138 | + return web.Response(status=404) |
| 139 | + return web.json_response(response) |
| 140 | + |
| 141 | + async def _http_handle_transaction(self, request): |
| 142 | + if not self._check_token(request): |
| 143 | + return web.Response(status=401) |
| 144 | + |
| 145 | + transaction_id = request.match_info["transaction_id"] |
| 146 | + if transaction_id in self.transactions: |
| 147 | + return web.Response(status=200) |
| 148 | + |
| 149 | + json = await request.json() |
| 150 | + |
| 151 | + try: |
| 152 | + events = json["events"] |
| 153 | + except KeyError: |
| 154 | + return web.Response(status=400) |
| 155 | + |
| 156 | + for event in events: |
| 157 | + self.handle_matrix_event(event) |
| 158 | + |
| 159 | + self.transactions.append(transaction_id) |
| 160 | + |
| 161 | + return web.json_response({}) |
| 162 | + |
| 163 | + async def update_state_store(self, event): |
| 164 | + event_type = event["type"] |
| 165 | + if event_type == "m.room.power_levels": |
| 166 | + self.state_store.set_power_levels(event["room_id"], event["content"]) |
| 167 | + elif event_type == "m.room.member": |
| 168 | + self.state_store.set_membership(event["room_id"], event["state_key"], |
| 169 | + event["content"]["membership"]) |
| 170 | + |
| 171 | + def handle_matrix_event(self, event): |
| 172 | + async def try_handle(handler): |
| 173 | + try: |
| 174 | + await handler(event) |
| 175 | + except Exception: |
| 176 | + self.log.exception("Exception in Matrix event handler") |
| 177 | + |
| 178 | + for handler in self.event_handlers: |
| 179 | + asyncio.ensure_future(try_handle(handler), loop=self.loop) |
| 180 | + |
| 181 | + def matrix_event_handler(self, func): |
| 182 | + self.event_handlers.append(func) |
| 183 | + return func |
0 commit comments