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