Merge branch 'master' of github.com:maubot/maubot

This commit is contained in:
Lorenz Steinert 2020-01-12 10:22:28 +01:00
commit 5dfad88cc3
No known key found for this signature in database
GPG key ID: 2584CA75748BFAE9
24 changed files with 3015 additions and 2109 deletions

View file

@ -36,3 +36,32 @@ push tag:
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
build standalone:
stage: build
script:
- docker pull $CI_REGISTRY_IMAGE:standalone || true
- docker build --pull --cache-from $CI_REGISTRY_IMAGE:standalone --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-standalone . -f maubot/standalone/Dockerfile
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-standalone
push latest standalone:
stage: push
only:
- master
variables:
GIT_STRATEGY: none
script:
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-standalone
- docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-standalone $CI_REGISTRY_IMAGE:standalone
- docker push $CI_REGISTRY_IMAGE:standalone
push tag standalone:
stage: push
variables:
GIT_STRATEGY: none
except:
- master
script:
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-standalone
- docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA-standalone $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME-standalone
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME-standalone

View file

@ -3,7 +3,7 @@ FROM node:12 AS frontend-builder
COPY ./maubot/management/frontend /frontend
RUN cd /frontend && yarn --prod && yarn build
FROM alpine:edge
FROM alpine:3.10
ENV UID=1337 \
GID=1337
@ -11,7 +11,7 @@ ENV UID=1337 \
COPY . /opt/maubot
COPY --from=frontend-builder /frontend/build /opt/maubot/frontend
WORKDIR /opt/maubot
RUN apk add --no-cache -X http://dl-cdn.alpinelinux.org/alpine/edge/testing \
RUN apk add --no-cache \
py3-aiohttp \
py3-sqlalchemy \
py3-attrs \
@ -24,7 +24,6 @@ RUN apk add --no-cache -X http://dl-cdn.alpinelinux.org/alpine/edge/testing \
py3-pillow \
py3-magic \
py3-psycopg2 \
py3-matplotlib \
py3-ruamel.yaml \
py3-jinja2 \
py3-click \

View file

@ -29,6 +29,8 @@ Matrix room: [#maubot:maunium.net](https://matrix.to/#/#maubot:maunium.net)
* [exec](https://github.com/maubot/exec) - A bot that executes code.
* [commitstrip](https://github.com/maubot/commitstrip) - A bot to view CommitStrips.
* [supportportal](https://github.com/maubot/supportportal) - A bot to manage customer support on Matrix.
* [gitlab](https://github.com/maubot/gitlab) - A GitLab client and webhook receiver.
* [github](https://github.com/maubot/github) - A GitHub client and webhook receiver.
Open a pull request or join the Matrix room linked above to get your plugin listed here

View file

@ -1 +1 @@
__version__ = "0.1.0.dev27"
__version__ = "0.1.0.dev30"

View file

@ -21,9 +21,10 @@ from aiohttp import ClientSession
from mautrix.errors import MatrixInvalidToken, MatrixRequestError
from mautrix.types import (UserID, SyncToken, FilterID, ContentURI, StrippedStateEvent, Membership,
EventType, Filter, RoomFilter, RoomEventFilter)
StateEvent, EventType, Filter, RoomFilter, RoomEventFilter)
from mautrix.client import InternalEventType
from .lib.store_proxy import ClientStoreProxy
from .db import DBClient
from .matrix import MaubotMatrixClient
@ -58,11 +59,13 @@ class Client:
self.remote_avatar_url = None
self.client = MaubotMatrixClient(mxid=self.id, base_url=self.homeserver,
token=self.access_token, client_session=self.http_client,
log=self.log, loop=self.loop, store=self.db_instance)
log=self.log, loop=self.loop,
store=ClientStoreProxy(self.db_instance))
self.client.ignore_initial_sync = True
self.client.ignore_first_sync = True
if self.autojoin:
self.client.add_event_handler(EventType.ROOM_MEMBER, self._handle_invite)
self.client.add_event_handler(EventType.ROOM_TOMBSTONE, self._handle_tombstone)
self.client.add_event_handler(InternalEventType.SYNC_ERRORED, self._set_sync_ok(False))
self.client.add_event_handler(InternalEventType.SYNC_SUCCESSFUL, self._set_sync_ok(True))
@ -107,13 +110,13 @@ class Client:
self.db_instance.enabled = False
return
if not self.filter_id:
self.db_instance.filter_id = await self.client.create_filter(Filter(
self.db_instance.edit(filter_id=await self.client.create_filter(Filter(
room=RoomFilter(
timeline=RoomEventFilter(
limit=50,
),
),
))
)))
if self.displayname != "disable":
await self.client.set_displayname(self.displayname)
if self.avatar_url != "disable":
@ -187,6 +190,13 @@ class Client:
def all(cls) -> Iterable['Client']:
return (cls.get(user.id, user) for user in DBClient.all())
async def _handle_tombstone(self, evt: StateEvent) -> None:
if not evt.content.replacement_room:
self.log.info(f"{evt.room_id} tombstoned with no replacement, ignoring")
return
_, server = self.client.parse_user_id(evt.sender)
await self.client.join_room(evt.content.replacement_room, servers=[server])
async def _handle_invite(self, evt: StrippedStateEvent) -> None:
if evt.state_key == self.id and evt.content.membership == Membership.INVITE:
await self.client.join_room(evt.room_id)

View file

@ -281,7 +281,7 @@ class RegexArgument(Argument):
def match(self, val: str, **kwargs) -> Tuple[str, Any]:
orig_val = val
if not self.pass_raw:
val = val.split(" ")[0]
val = re.split(r"\s", val, 1)[0]
match = self.regex.match(val)
if match:
return (orig_val[:match.start()] + orig_val[match.end():],
@ -299,7 +299,7 @@ class CustomArgument(Argument):
if self.pass_raw:
return self.matcher(val)
orig_val = val
val = val.split(" ")[0]
val = re.split(r"\s", val, 1)[0]
res = self.matcher(val)
if res:
return orig_val[len(val):], res
@ -310,7 +310,7 @@ class SimpleArgument(Argument):
def match(self, val: str, **kwargs) -> Tuple[str, Any]:
if self.pass_raw:
return "", val
res = val.split(" ")[0]
res = re.split(r"\s", val, 1)[0]
return val[len(res):], res

View file

@ -39,6 +39,7 @@ log = logging.getLogger("maubot.instance")
yaml = YAML()
yaml.indent(4)
yaml.width = 200
class PluginInstance:

30
maubot/lib/store_proxy.py Normal file
View file

@ -0,0 +1,30 @@
# maubot - A plugin-based Matrix bot system.
# Copyright (C) 2019 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# 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 mautrix.client import ClientStore
from mautrix.types import SyncToken
class ClientStoreProxy(ClientStore):
def __init__(self, db_instance) -> None:
self.db_instance = db_instance
@property
def next_batch(self) -> SyncToken:
return self.db_instance.next_batch
@next_batch.setter
def next_batch(self, value: SyncToken) -> None:
self.db_instance.edit(next_batch=value)

View file

@ -57,6 +57,7 @@ class PluginMeta(SerializableAttrs['PluginMeta']):
maubot: Version = Version(__version__)
database: bool = False
config: bool = False
webapp: bool = False
license: str = ""
extra_files: List[str] = []

View file

@ -18,6 +18,7 @@ from http import HTTPStatus
from aiohttp import web
from sqlalchemy.exc import OperationalError, IntegrityError
class _Response:
@property
def body_not_json(self) -> web.Response:

View file

@ -15,12 +15,12 @@
"dependencies": {
"node-sass": "^4.12.0",
"react": "^16.8.6",
"react-ace": "^7.0.2",
"react-ace": "^8.0.0",
"react-contextmenu": "^2.11.0",
"react-dom": "^16.8.6",
"react-json-tree": "^0.11.2",
"react-router-dom": "^5.0.1",
"react-scripts": "3.0.1",
"react-scripts": "3.3.0",
"react-select": "^3.0.4"
},
"scripts": {

View file

@ -30,7 +30,8 @@ class Login extends Component {
inputChanged = event => this.setState({ [event.target.name]: event.target.value })
login = async () => {
login = async evt => {
evt.preventDefault()
this.setState({ loading: true })
const resp = await api.login(this.state.username, this.state.password)
if (resp.token) {
@ -53,17 +54,17 @@ class Login extends Component {
</div>
}
return <div className="login-wrapper">
<div className={`login ${this.state.error && "errored"}`}>
<form className={`login ${this.state.error && "errored"}`} onSubmit={this.login}>
<h1>Maubot Manager</h1>
<input type="text" placeholder="Username" value={this.state.username}
name="username" onChange={this.inputChanged}/>
<input type="password" placeholder="Password" value={this.state.password}
name="password" onChange={this.inputChanged}/>
<button onClick={this.login}>
<button onClick={this.login} type="submit">
{this.state.loading ? <Spinner/> : "Log in"}
</button>
{this.state.error && <div className="error">{this.state.error}</div>}
</div>
</form>
</div>
}
}

View file

@ -16,8 +16,8 @@
import React from "react"
import { Link, NavLink, Route, Switch, withRouter } from "react-router-dom"
import AceEditor from "react-ace"
import "brace/mode/yaml"
import "brace/theme/github"
import "ace-builds/src-noconflict/mode-yaml"
import "ace-builds/src-noconflict/theme-github"
import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg"
import { ReactComponent as NoAvatarIcon } from "../../res/bot.svg"
import PrefTable, { PrefInput, PrefSelect, PrefSwitch } from "../../components/PreferenceTable"

View file

@ -22,6 +22,7 @@ import { ReactComponent as OrderAsc } from "../../res/sort-up.svg"
import api from "../../api"
import Spinner from "../../components/Spinner"
// eslint-disable-next-line no-extend-native
Map.prototype.map = function(func) {
const res = []
for (const [key, value] of this) {
@ -89,7 +90,7 @@ class InstanceDatabase extends Component {
}
buildSQLQuery(table = this.state.selectedTable, resetContent = true) {
let query = `SELECT * FROM ${table}`
let query = `SELECT * FROM "${table}"`
if (this.order.size > 0) {
const order = Array.from(this.order.entries()).reverse()
@ -197,10 +198,10 @@ class InstanceDatabase extends Component {
const val = values[index]
condition.push(`${key}='${this.sqlEscape(val.toString())}'`)
}
const query = `DELETE FROM ${this.state.selectedTable} WHERE ${condition.join(" AND ")}`
const query = `DELETE FROM "${this.state.selectedTable}" WHERE ${condition.join(" AND ")}`
const res = await api.queryInstanceDatabase(this.props.instanceID, query)
this.setState({
prevQuery: `DELETE FROM ${this.state.selectedTable} ...`,
prevQuery: `DELETE FROM "${this.state.selectedTable}" ...`,
rowCount: res.rowcount,
})
await this.reloadContent(false)
@ -215,6 +216,7 @@ class InstanceDatabase extends Component {
col: props.col,
})
// eslint-disable-next-line no-control-regex
sqlEscape = str => str.replace(/[\0\x08\x09\x1a\n\r"'\\%]/g, char => {
switch (char) {
case "\0":

View file

@ -2,29 +2,29 @@
font-family: 'Raleway'
font-style: normal
font-weight: 300
src: local('Raleway Light'), local('Raleway-Light'), url('../fonts/raleway-light.woff2') format('woff2')
src: local('Raleway Light'), local('Raleway-Light'), url('../../fonts/raleway-light.woff2') format('woff2')
@font-face
font-family: 'Raleway'
font-style: normal
font-weight: 400
src: local('Raleway'), local('Raleway-Regular'), url('../fonts/raleway-regular.woff2') format('woff2')
src: local('Raleway'), local('Raleway-Regular'), url('../../fonts/raleway-regular.woff2') format('woff2')
@font-face
font-family: 'Raleway'
font-style: normal
font-weight: 700
src: local('Raleway Bold'), local('Raleway-Bold'), url('../fonts/raleway-bold.woff2') format('woff2')
src: local('Raleway Bold'), local('Raleway-Bold'), url('../../fonts/raleway-bold.woff2') format('woff2')
@font-face
font-family: 'Fira Code'
src: url('../fonts/firacode-regular.woff2') format('woff2')
src: url('../../fonts/firacode-regular.woff2') format('woff2')
font-weight: 400
font-style: normal
@font-face
font-family: 'Fira Code'
src: url('../fonts/firacode-bold.woff2') format('woff2')
src: url('../../fonts/firacode-bold.woff2') format('woff2')
font-weight: 700
font-style: normal

File diff suppressed because it is too large Load diff

View file

@ -14,27 +14,24 @@
# 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 Union, Awaitable, Optional, Tuple
from markdown.extensions import Extension
import markdown as md
from html import escape
import attr
from mautrix.client import Client as MatrixClient, SyncStream
from mautrix.util.formatter import parse_html
from mautrix.util import markdown
from mautrix.types import (EventType, MessageEvent, Event, EventID, RoomID, MessageEventContent,
MessageType, TextMessageEventContent, Format, RelatesTo)
class EscapeHTML(Extension):
def extendMarkdown(self, md):
md.preprocessors.deregister("html_block")
md.inlinePatterns.deregister("html")
escape_html = EscapeHTML()
def parse_markdown(markdown: str, allow_html: bool = False) -> Tuple[str, str]:
html = md.markdown(markdown, extensions=[escape_html] if not allow_html else [])
def parse_formatted(message: str, allow_html: bool = False, render_markdown: bool = True
) -> Tuple[str, str]:
if render_markdown:
html = markdown.render(message, allow_html=allow_html)
elif allow_html:
html = message
else:
return message, escape(message)
return parse_html(html), html
@ -50,14 +47,15 @@ class MaubotMessageEvent(MessageEvent):
def respond(self, content: Union[str, MessageEventContent],
event_type: EventType = EventType.ROOM_MESSAGE, markdown: bool = True,
html_in_markdown: bool = False, reply: bool = False,
allow_html: bool = False, reply: bool = False,
edits: Optional[Union[EventID, MessageEvent]] = None) -> Awaitable[EventID]:
if isinstance(content, str):
content = TextMessageEventContent(msgtype=MessageType.NOTICE, body=content)
if markdown:
if allow_html or markdown:
content.format = Format.HTML
content.body, content.formatted_body = parse_markdown(content.body,
allow_html=html_in_markdown)
content.body, content.formatted_body = parse_formatted(content.body,
render_markdown=markdown,
allow_html=allow_html)
if edits:
content.set_edit(edits)
elif reply and not self.disable_reply:
@ -66,9 +64,9 @@ class MaubotMessageEvent(MessageEvent):
def reply(self, content: Union[str, MessageEventContent],
event_type: EventType = EventType.ROOM_MESSAGE, markdown: bool = True,
html_in_markdown: bool = False) -> Awaitable[EventID]:
return self.respond(content, event_type, markdown, reply=True,
html_in_markdown=html_in_markdown)
allow_html: bool = False) -> Awaitable[EventID]:
return self.respond(content, event_type, markdown=markdown, reply=True,
allow_html=allow_html)
def mark_read(self) -> Awaitable[None]:
return self.client.send_receipt(self.room_id, self.event_id, "m.read")
@ -78,17 +76,19 @@ class MaubotMessageEvent(MessageEvent):
def edit(self, content: Union[str, MessageEventContent],
event_type: EventType = EventType.ROOM_MESSAGE, markdown: bool = True,
html_in_markdown: bool = False) -> Awaitable[EventID]:
return self.respond(content, event_type, markdown, edits=self,
html_in_markdown=html_in_markdown)
allow_html: bool = False) -> Awaitable[EventID]:
return self.respond(content, event_type, markdown=markdown, edits=self,
allow_html=allow_html)
class MaubotMatrixClient(MatrixClient):
def send_markdown(self, room_id: RoomID, markdown: str, msgtype: MessageType = MessageType.TEXT,
def send_markdown(self, room_id: RoomID, markdown: str, *, allow_html: bool = False,
msgtype: MessageType = MessageType.TEXT,
edits: Optional[Union[EventID, MessageEvent]] = None,
relates_to: Optional[RelatesTo] = None, **kwargs) -> Awaitable[EventID]:
relates_to: Optional[RelatesTo] = None, **kwargs
) -> Awaitable[EventID]:
content = TextMessageEventContent(msgtype=msgtype, format=Format.HTML)
content.body, content.formatted_body = parse_markdown(markdown)
content.body, content.formatted_body = parse_formatted(markdown, allow_html=allow_html)
if relates_to:
if edits:
raise ValueError("Can't use edits and relates_to at the same time.")

View file

@ -20,6 +20,7 @@ from asyncio import AbstractEventLoop
from sqlalchemy.engine.base import Engine
from aiohttp import ClientSession
from yarl import URL
if TYPE_CHECKING:
from mautrix.util.config import BaseProxyConfig
@ -35,7 +36,7 @@ class Plugin(ABC):
config: Optional['BaseProxyConfig']
database: Optional[Engine]
webapp: Optional['PluginWebApp']
webapp_url: Optional[str]
webapp_url: Optional[URL]
def __init__(self, client: 'MaubotMatrixClient', loop: AbstractEventLoop, http: ClientSession,
instance_id: str, log: Logger, config: Optional['BaseProxyConfig'],
@ -49,12 +50,12 @@ class Plugin(ABC):
self.config = config
self.database = database
self.webapp = webapp
self.webapp_url = webapp_url
self.webapp_url = URL(webapp_url) if webapp_url else None
self._handlers_at_startup = []
async def internal_start(self) -> None:
for key in dir(self):
val = getattr(self, key)
def register_handler_class(self, obj) -> None:
for key in dir(obj):
val = getattr(obj, key)
try:
if val.__mb_event_handler__:
self._handlers_at_startup.append((val, val.__mb_event_type__))
@ -67,12 +68,23 @@ class Plugin(ABC):
self.webapp.add_route(method=method, path=path, handler=val, **kwargs)
except AttributeError:
pass
async def pre_start(self) -> None:
pass
async def internal_start(self) -> None:
await self.pre_start()
self.register_handler_class(self)
await self.start()
async def start(self) -> None:
pass
async def pre_stop(self) -> None:
pass
async def internal_stop(self) -> None:
await self.pre_stop()
for func, event_type in self._handlers_at_startup:
self.client.remove_event_handler(event_type, func)
if self.webapp is not None:

View file

@ -0,0 +1,23 @@
FROM docker.io/alpine:3.10
COPY . /opt/maubot
RUN cd /opt/maubot \
&& apk add --no-cache --virtual .build-deps \
python3-dev \
libffi-dev \
build-base \
&& apk add --no-cache \
py3-aiohttp \
py3-sqlalchemy \
py3-attrs \
py3-bcrypt \
py3-cffi \
ca-certificates \
su-exec \
py3-psycopg2 \
py3-ruamel.yaml \
py3-jinja2 \
py3-packaging \
py3-markdown \
&& pip3 install . \
&& apk del .build-deps

View file

View file

@ -0,0 +1,212 @@
# supportportal - A maubot plugin to manage customer support on Matrix.
# Copyright (C) 2019 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# 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 Optional
from aiohttp import ClientSession
import logging.config
import importlib
import argparse
import asyncio
import signal
import copy
import sys
from ruamel.yaml import YAML
from ruamel.yaml.comments import CommentedMap
import sqlalchemy as sql
from mautrix.util.config import RecursiveDict
from mautrix.util.db import Base
from mautrix.types import (UserID, Filter, RoomFilter, RoomEventFilter, StrippedStateEvent,
EventType, Membership)
from .config import Config
from ..plugin_base import Plugin
from ..loader import PluginMeta
from ..matrix import MaubotMatrixClient
from ..lib.store_proxy import ClientStoreProxy
from ..__meta__ import __version__
from supportportal import SupportPortalBot
parser = argparse.ArgumentParser(
description="A plugin-based Matrix bot system -- standalone mode.",
prog="python -m maubot.standalone")
parser.add_argument("-c", "--config", type=str, default="config.yaml",
metavar="<path>", help="the path to your config file")
parser.add_argument("-b", "--base-config", type=str, default="example-config.yaml",
metavar="<path>", help="the path to the example config "
"(for automatic config updates)")
parser.add_argument("-m", "--meta", type=str, default="maubot.yaml",
metavar="<path>", help="the path to your plugin metadata file")
args = parser.parse_args()
config = Config(args.config, args.base_config)
config.load()
try:
config.update()
except Exception as e:
print("Failed to update config:", e)
logging.config.dictConfig(copy.deepcopy(config["logging"]))
log = logging.getLogger("maubot.init")
log.debug(f"Loading plugin metadata from {args.meta}")
yaml = YAML()
with open(args.meta, "r") as meta_file:
meta: PluginMeta = PluginMeta.deserialize(yaml.load(meta_file.read()))
if "/" in meta.main_class:
module, main_class = meta.main_class.split("/", 1)
else:
module = meta.modules[0]
main_class = meta.main_class
bot_module = importlib.import_module(module)
plugin = getattr(bot_module, main_class)
log.info(f"Initializing standalone {meta.id} v{meta.version} on maubot {__version__}")
class NextBatch(Base):
__tablename__ = "standalone_next_batch"
user_id: str = sql.Column(sql.String(255), primary_key=True)
next_batch: str = sql.Column(sql.String(255))
filter_id: str = sql.Column(sql.String(255))
@classmethod
def get(cls, user_id: UserID) -> Optional['NextBatch']:
return cls._select_one_or_none(cls.c.user_id == user_id)
log.debug("Opening database")
db = sql.create_engine(config["database"])
Base.metadata.bind = db
Base.metadata.create_all()
NextBatch.bind(db)
user_id = config["user.credentials.id"]
homeserver = config["user.credentials.homeserver"]
access_token = config["user.credentials.access_token"]
nb = NextBatch.get(user_id)
if not nb:
nb = NextBatch(user_id=user_id, next_batch="", filter_id="")
nb.insert()
bot_config = None
if meta.config:
log.debug("Loading config")
config_class = SupportPortalBot.get_config_class()
def load() -> CommentedMap:
return config["plugin_config"]
def load_base() -> RecursiveDict[CommentedMap]:
return RecursiveDict(config.load_base()["plugin_config"], CommentedMap)
def save(data: RecursiveDict[CommentedMap]) -> None:
config["plugin_config"] = data
config.save()
try:
bot_config = config_class(load=load, load_base=load_base, save=save)
bot_config.load_and_update()
except Exception:
log.fatal("Failed to load plugin config", exc_info=True)
sys.exit(1)
loop = asyncio.get_event_loop()
client: MaubotMatrixClient = None
bot: Plugin = None
async def main():
http_client = ClientSession(loop=loop)
global client, bot
client = MaubotMatrixClient(mxid=user_id, base_url=homeserver, token=access_token,
client_session=http_client, loop=loop, store=ClientStoreProxy(nb),
log=logging.getLogger("maubot.client").getChild(user_id))
while True:
try:
whoami_user_id = await client.whoami()
except Exception:
log.exception("Failed to connect to homeserver, retrying in 10 seconds...")
await asyncio.sleep(10)
continue
if whoami_user_id != user_id:
log.fatal(f"User ID mismatch: configured {user_id}, but server said {whoami_user_id}")
sys.exit(1)
break
if config["user.sync"]:
if not nb.filter_id:
nb.edit(filter_id=await client.create_filter(Filter(
room=RoomFilter(timeline=RoomEventFilter(limit=50)),
)))
client.start(nb.filter_id)
if config["autojoin"]:
log.debug("Autojoin is enabled")
@client.on(EventType.ROOM_MEMBER)
async def _handle_invite(evt: StrippedStateEvent) -> None:
if evt.state_key == client.mxid and evt.content.membership == Membership.INVITE:
await client.join_room(evt.room_id)
displayname, avatar_url = config["user.displayname"], config["user.avatar_url"]
if avatar_url != "disable":
await client.set_avatar_url(avatar_url)
if displayname != "disable":
await client.set_displayname(displayname)
bot = plugin(client=client, loop=loop, http=http_client, instance_id="__main__",
log=logging.getLogger("maubot.instance.__main__"), config=bot_config,
database=db if meta.database else None, webapp=None, webapp_url=None)
await bot.internal_start()
try:
log.info("Starting plugin")
loop.run_until_complete(main())
except Exception:
log.fatal("Failed to start plugin", exc_info=True)
sys.exit(1)
signal.signal(signal.SIGINT, signal.default_int_handler)
signal.signal(signal.SIGTERM, signal.default_int_handler)
try:
log.info("Startup completed, running forever")
loop.run_forever()
except KeyboardInterrupt:
log.info("Interrupt received, stopping")
client.stop()
loop.run_until_complete(bot.internal_stop())
loop.close()
sys.exit(0)
except Exception:
log.fatal("Fatal error in bot", exc_info=True)
sys.exit(1)

View file

@ -0,0 +1,37 @@
# maubot - A plugin-based Matrix bot system.
# Copyright (C) 2019 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# 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 mautrix.util.config import BaseFileConfig, ConfigUpdateHelper
class Config(BaseFileConfig):
def do_update(self, helper: ConfigUpdateHelper) -> None:
copy, _, base = helper
copy("user.credentials.id")
copy("user.credentials.homeserver")
copy("user.credentials.access_token")
copy("user.sync")
copy("user.autojoin")
copy("user.displayname")
copy("user.avatar_url")
if "database" in base:
copy("database")
if "plugin_config" in base:
copy("plugin_config")
if "server" in base:
copy("server.hostname")
copy("server.port")
copy("server.public_url")
copy("logging")

View file

@ -2,7 +2,7 @@ mautrix
aiohttp
SQLAlchemy
alembic
Markdown
commonmark
ruamel.yaml
attrs
bcrypt

View file

@ -22,11 +22,11 @@ setuptools.setup(
packages=setuptools.find_packages(),
install_requires=[
"mautrix>=0.4.dev72,<0.5",
"mautrix>=0.4,<0.5",
"aiohttp>=3.0.1,<4",
"SQLAlchemy>=1.2.3,<2",
"alembic>=1.0.0,<2",
"Markdown>=3.0.0,<4",
"commonmark>=0.9.1,<1",
"ruamel.yaml>=0.15.35,<0.17",
"attrs>=18.1.0",
"bcrypt>=3.1.4,<4",
@ -47,6 +47,7 @@ setuptools.setup(
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
],
entry_points="""
[console_scripts]