first commit

This commit is contained in:
Dave Umrysh 2021-03-19 12:16:21 -06:00
commit a2e6d171a1
9 changed files with 993 additions and 0 deletions

4
.htaccess Normal file
View File

@ -0,0 +1,4 @@
<Files webhook.log>
order deny,allow
deny from all
</Files>

661
LICENSE Normal file

File diff suppressed because it is too large Load Diff

40
README.md Normal file
View File

@ -0,0 +1,40 @@
incoming webhook
==============
An incoming webhook maubot based on the maugitea plugin
WARNING: This bot is still under development, so things may change, break, or even work properly.
Install & Run
------------
icoming_webhook: just a regular plugin, zip it and upload it.
Webhooks
------------
url: `https://fancy.domain/_matrix/maubot/plugin/<instance_name>/webhook/r0?room=<room-id>`
Add the secret to base-config.yaml
* webhook-secret: "your secret here"
Bot usage
------------
Add the bot to the room and then when you POST a message to the webhook URL the bot will post the message to the room.
Example:
```
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,$web_hook_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(array('secret' => $web_hook_secret, 'message' => "Here is a test message ")));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt( $ch, CURLOPT_HTTPHEADER, array('Content-Type:application/json'));
$result = curl_exec($ch);
curl_close($ch);
?>
```

2
base-config.yaml Normal file
View File

@ -0,0 +1,2 @@
webhook-secret: "BKft2caVAYoQV9Z6j4E3Gnhhgn"
send_as_notice: false

View File

@ -0,0 +1 @@
from .bot import WebhookBot

120
incoming_webhook/bot.py Normal file
View File

@ -0,0 +1,120 @@
# 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 List, Set, Type
from functools import partial
from aiohttp.web import Response, Request
import asyncio
from asyncio import Task
from yarl import URL
from maubot import Plugin, MessageEvent
from maubot.handlers import command, event, web
from mautrix.types import EventType, Membership, MessageType, RoomID, StateEvent
from mautrix.util.config import BaseProxyConfig
from .db import Database
from .config import Config
from pprint import pprint
class WebhookBot(Plugin):
task_list: List[Task]
joined_rooms: Set[RoomID]
async def start(self) -> None:
await super().start()
self.config.load_and_update()
self.db = Database(self.database)
self.joined_rooms = set(await self.client.get_joined_rooms())
self.task_list = []
async def stop(self) -> None:
if self.task_list:
await asyncio.wait(self.task_list, timeout=1)
@classmethod
def get_config_class(cls) -> Type[BaseProxyConfig]:
return Config
@event.on(EventType.ROOM_MEMBER)
async def member_handler(self, evt: StateEvent) -> None:
"""
updates the stored joined_rooms object whenever
the bot joins or leaves a room.
"""
if evt.state_key != self.client.mxid:
return
if evt.content.membership in (Membership.LEAVE, Membership.BAN):
self.joined_rooms.remove(evt.room_id)
if evt.content.membership == Membership.JOIN and evt.state_key == self.client.mxid:
self.joined_rooms.add(evt.room_id)
# region Webhook handling
@web.post("/webhook/r0")
async def post_handler(self, request: Request) -> Response:
if "room" not in request.query:
return Response(text="400: Bad request\n"
"No room specified. Did you forget the '?room=' query parameter?\n",
status=400)
if request.query["room"] not in self.joined_rooms:
return Response(text="403: Forbidden\nThe bot is not in the room. "
f"Please invite the bot to the room.\n", status=403)
if request.headers.getone("Content-Type", "") != "application/json":
return Response(status=406, text="406: Not Acceptable\n",
headers={"Accept": "application/json"})
if not request.can_read_body:
return Response(status=400, text="400: Bad request\n"
"Missing request body\n")
task = self.loop.create_task(self.process_hook_01(request))
self.task_list += [task]
return Response(status=202, text="202: Accepted\nWebhook processing started.\n")
async def process_hook_01(self, req: Request) -> None:
if self.config["send_as_notice"]:
msgtype = MessageType.NOTICE
else:
msgtype = MessageType.TEXT
try:
msg = None
body = await req.json()
if body["secret"] != self.config["webhook-secret"]:
self.log.error("Failed to handle event: secret doesnt match.")
else:
msg = (f"{body['message']}")
room_id = RoomID(req.query["room"])
if msg:
event_id = await self.client.send_markdown(room_id, msg, allow_html=True, msgtype=msgtype)
except Exception:
self.log.error("Failed to handle event", exc_info=True)
task = asyncio.current_task()
if task:
self.task_list.remove(task)
# endregion

View File

@ -0,0 +1,18 @@
# 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 BaseProxyConfig, ConfigUpdateHelper
class Config(BaseProxyConfig):
def do_update(self, helper: ConfigUpdateHelper) -> None:
pass

134
incoming_webhook/db.py Normal file
View File

@ -0,0 +1,134 @@
# 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 List, NamedTuple
from sqlalchemy import Column, ForeignKey, ForeignKeyConstraint, String, Text, or_
from sqlalchemy.engine.base import Engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session, relationship
from mautrix.types import UserID
AuthInfo = NamedTuple('AuthInfo', server=str, api_token=str)
AliasInfo = NamedTuple('AliasInfo', server=str, alias=str)
Base = declarative_base()
from pprint import pprint
class ServerAlias(Base):
__tablename__ = "serveralias"
user_id: UserID = Column(String(255), primary_key=True)
alias = Column(Text, primary_key=True, nullable=False)
gitea_server = Column(Text, primary_key=True)
class ServerToken(Base):
__tablename__ = "servertoken"
user_id: UserID = Column(String(255), primary_key=True, nullable=False)
gitea_server = Column(Text, primary_key=True, nullable=False)
api_token = Column(Text, nullable=False)
class RepositoryAlias(Base):
__tablename__ = "repositoryalias"
user_id: UserID = Column(String(255), primary_key=True)
alias = Column(Text, primary_key=True, nullable=False)
gitea_repository = Column(Text, primary_key=True)
class Database:
db: Engine
def __init__(self, db: Engine) -> None:
self.db = db
Base.metadata.create_all(db)
self.Session = sessionmaker(bind=self.db)
def add_server_alias(self, mxid: UserID, url: str, alias: str) -> None:
s = self.Session()
salias = ServerAlias(user_id=mxid, gitea_server=url, alias=alias)
s.add(salias)
s.commit()
def get_server_aliases(self, user_id: UserID) -> List[AliasInfo]:
s = self.Session()
rows = s.query(ServerAlias).filter(ServerAlias.user_id == user_id)
return [AliasInfo(row.gitea_server, row.alias) for row in rows]
def get_server_alias(self, user_id: UserID, alias: str) -> str:
s = self.Session()
row = s.query(ServerAlias).filter(ServerAlias.user_id == user_id, ServerAlias.alias == alias).scalar()
if row:
return row.gitea_server
return None
def has_server_alias(self, user_id: UserID, alias: str) -> bool:
s: Session = self.Session()
return s.query(ServerAlias).filter(ServerAlias.user_id == user_id, ServerAlias.alias == alias).count() > 0
def rm_server_alias(self, mxid: UserID, alias: str) -> None:
s = self.Session()
alias = s.query(ServerAlias).filter(ServerAlias.user_id == mxid,
ServerAlias.alias == alias).one()
s.delete(alias)
s.commit()
def get_servers(self, mxid: UserID) -> List[str]:
s = self.Session()
rows = s.query(ServerToken).filter(ServerToken.user_id == mxid)
return [row.gitea_server for row in rows]
def add_login(self, mxid: UserID, url: str, token: str) -> None:
s = self.Session()
s.add(ServerToken(user_id=mxid, gitea_server=url, api_token=token))
s.commit()
def rm_login(self, mxid: UserID, url: str) -> None:
s = self.Session()
token = s.query(ServerToken).get((mxid, url))
s.delete(token)
s.commit()
def get_login(self, mxid: UserID, url: str) -> AuthInfo:
s = self.Session()
row = s.query(ServerToken).filter(ServerToken.user_id == mxid, ServerToken.gitea_server == url).one()
return AuthInfo(server=row.gitea_server, api_token=row.api_token)
def add_repos_alias(self, mxid: UserID, repos: str, alias: str) -> None:
s = self.Session()
ralias = RepositoryAlias(user_id=mxid, gitea_repository=repos, alias=alias)
s.add(ralias)
s.commit()
def get_repos_aliases(self, user_id: UserID) -> List[AliasInfo]:
s = self.Session()
rows = s.query(RepositoryAlias).filter(RepositoryAlias.user_id == user_id)
return [AliasInfo(row.gitea_repository, row.alias) for row in rows]
def get_repos_alias(self, user_id: UserID, alias: str) -> str:
s = self.Session()
row = s.query(RepositoryAlias).filter(RepositoryAlias.user_id == user_id, RepositoryAlias.alias == alias).scalar()
if row:
return row.gitea_repository
return None
def has_repos_alias(self, user_id: UserID, alias: str) -> bool:
s: Session = self.Session()
return s.query(RepositoryAlias).filter(RepositoryAlias.user_id == user_id, RepositoryAlias.alias == alias).count() > 0
def rm_repos_alias(self, mxid: UserID, alias: str) -> None:
s = self.Session()
ralias = s.query(RepositoryAlias).filter(RepositoryAlias.user_id == mxid,
RepositoryAlias.alias == alias).one()
s.delete(ralias)
s.commit()

13
maubot.yaml Normal file
View File

@ -0,0 +1,13 @@
id: com.umycode.incoming
version: 0.0.2
modules:
- incoming_webhook
main_class: WebhookBot
maubot: 0.1.0
database: true
webapp: true
license: AGPL-3.0-or-later
extra_files:
- base-config.yaml
dependencies: []
soft_dependencies: []