first commit
This commit is contained in:
commit
a2e6d171a1
4
.htaccess
Normal file
4
.htaccess
Normal file
@ -0,0 +1,4 @@
|
||||
<Files webhook.log>
|
||||
order deny,allow
|
||||
deny from all
|
||||
</Files>
|
40
README.md
Normal file
40
README.md
Normal 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
2
base-config.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
webhook-secret: "BKft2caVAYoQV9Z6j4E3Gnhhgn"
|
||||
send_as_notice: false
|
1
incoming_webhook/__init__.py
Normal file
1
incoming_webhook/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .bot import WebhookBot
|
120
incoming_webhook/bot.py
Normal file
120
incoming_webhook/bot.py
Normal 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
|
18
incoming_webhook/config.py
Normal file
18
incoming_webhook/config.py
Normal 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
134
incoming_webhook/db.py
Normal 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
13
maubot.yaml
Normal 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: []
|
Loading…
Reference in New Issue
Block a user