Skip to content

Commit

Permalink
Initial commit. Transfer from mautrix-telegram
Browse files Browse the repository at this point in the history
  • Loading branch information
tulir committed Mar 8, 2018
0 parents commit 149c100
Show file tree
Hide file tree
Showing 12 changed files with 1,713 additions and 0 deletions.
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
indent_style = tab
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.{yaml,yml,py}]
indent_style = space
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.idea/

build/
dist/
mautrix_appservice.egg-info

.venv
pip-selfcheck.json
*.pyc
__pycache__
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# mautrix-appservice
A Python 3 asyncio-based Matrix application service framework.
5 changes: 5 additions & 0 deletions mautrix_appservice/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .appservice import AppService
from .errors import MatrixError, MatrixRequestError, IntentError

__version__ = "0.1.0"
__author__ = "Tulir Asokan <tulir@maunium.net>"
183 changes: 183 additions & 0 deletions mautrix_appservice/appservice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# -*- coding: future_fstrings -*-
# matrix-appservice-python - A Matrix Application Service framework written in Python.
# Copyright (C) 2018 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Partly based on github.com/Cadair/python-appservice-framework (MIT license)
from contextlib import contextmanager
from aiohttp import web
import aiohttp
import asyncio
import logging

from .intent_api import HTTPAPI
from .state_store import StateStore


class AppService:
def __init__(self, server, domain, as_token, hs_token, bot_localpart, loop=None, log=None,
verify_ssl=True, query_user=None, query_alias=None):
self.server = server
self.domain = domain
self.verify_ssl = verify_ssl
self.as_token = as_token
self.hs_token = hs_token
self.bot_mxid = f"@{bot_localpart}:{domain}"
self.state_store = StateStore(autosave_file="mx-state.json")
self.state_store.load("mx-state.json")

self.transactions = []

self._http_session = None
self._intent = None

self.loop = loop or asyncio.get_event_loop()
self.log = (logging.getLogger(log) if isinstance(log, str)
else log or logging.getLogger("mautrix_appservice"))

async def default_query_handler(_):
return None

self.query_user = query_user or default_query_handler
self.query_alias = query_alias or default_query_handler

self.event_handlers = []

self.app = web.Application(loop=self.loop)
self.app.router.add_route("PUT", "/transactions/{transaction_id}",
self._http_handle_transaction)
self.app.router.add_route("GET", "/rooms/{alias}", self._http_query_alias)
self.app.router.add_route("GET", "/users/{user_id}", self._http_query_user)

self.matrix_event_handler(self.update_state_store)

@property
def http_session(self):
if self._http_session is None:
raise AttributeError("the http_session attribute can only be used "
"from within the `AppService.run` context manager")
else:
return self._http_session

@property
def intent(self):
if self._intent is None:
raise AttributeError("the intent attribute can only be used from "
"within the `AppService.run` context manager")
else:
return self._intent

@contextmanager
def run(self, host="127.0.0.1", port=8080):
connector = None
if self.server.startswith("https://") and not self.verify_ssl:
connector = aiohttp.TCPConnector(verify_ssl=False)
self._http_session = aiohttp.ClientSession(loop=self.loop, connector=connector)
self._intent = HTTPAPI(base_url=self.server, domain=self.domain, bot_mxid=self.bot_mxid,
token=self.as_token, log=self.log, state_store=self.state_store,
client_session=self._http_session).bot_intent()

yield self.loop.create_server(self.app.make_handler(), host, port)

self._intent = None
self._http_session.close()
self._http_session = None

def _check_token(self, request):
try:
token = request.rel_url.query["access_token"]
except KeyError:
return False

if token != self.hs_token:
return False

return True

async def _http_query_user(self, request):
if not self._check_token(request):
return web.Response(status=401)

user_id = request.match_info["userId"]

try:
response = await self.query_user(user_id)
except Exception:
self.log.exception("Exception in user query handler")
return web.Response(status=500)

if not response:
return web.Response(status=404)
return web.json_response(response)

async def _http_query_alias(self, request):
if not self._check_token(request):
return web.Response(status=401)

alias = request.match_info["alias"]

try:
response = await self.query_alias(alias)
except Exception:
self.log.exception("Exception in alias query handler")
return web.Response(status=500)

if not response:
return web.Response(status=404)
return web.json_response(response)

async def _http_handle_transaction(self, request):
if not self._check_token(request):
return web.Response(status=401)

transaction_id = request.match_info["transaction_id"]
if transaction_id in self.transactions:
return web.Response(status=200)

json = await request.json()

try:
events = json["events"]
except KeyError:
return web.Response(status=400)

for event in events:
self.handle_matrix_event(event)

self.transactions.append(transaction_id)

return web.json_response({})

async def update_state_store(self, event):
event_type = event["type"]
if event_type == "m.room.power_levels":
self.state_store.set_power_levels(event["room_id"], event["content"])
elif event_type == "m.room.member":
self.state_store.set_membership(event["room_id"], event["state_key"],
event["content"]["membership"])

def handle_matrix_event(self, event):
async def try_handle(handler):
try:
await handler(event)
except Exception:
self.log.exception("Exception in Matrix event handler")

for handler in self.event_handlers:
asyncio.ensure_future(try_handle(handler), loop=self.loop)

def matrix_event_handler(self, func):
self.event_handlers.append(func)
return func
38 changes: 38 additions & 0 deletions mautrix_appservice/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# -*- coding: future_fstrings -*-
# mautrix-telegram - A Matrix-Telegram puppeting bridge
# Copyright (C) 2018 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.


class MatrixError(Exception):
"""A generic Matrix error. Specific errors will subclass this."""
pass


class IntentError(MatrixError):
def __init__(self, message, source):
super().__init__(message)
self.source = source


class MatrixRequestError(MatrixError):
""" The home server returned an error response. """

def __init__(self, code=0, text="", errcode=None, message=None):
super().__init__(f"{code}: {text}")
self.code = code
self.text = text
self.errcode = errcode
self.message = message
Loading

0 comments on commit 149c100

Please sign in to comment.