2021-03-15 23:02:27 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
|
|
import asyncio
|
|
|
|
import dataclasses
|
2021-03-15 23:20:54 +01:00
|
|
|
import logging
|
2021-03-15 23:59:36 +01:00
|
|
|
import typing
|
2021-03-15 23:02:27 +01:00
|
|
|
|
|
|
|
import aiohttp.web
|
|
|
|
import jinja2
|
|
|
|
import markdown
|
|
|
|
import nio
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
|
|
|
@dataclasses.dataclass
|
|
|
|
class Config:
|
|
|
|
matrix_homeserver: str
|
|
|
|
matrix_user: str
|
|
|
|
matrix_password: str
|
2021-03-15 23:59:36 +01:00
|
|
|
matrix_rooms: typing.Mapping[int, str]
|
2021-03-15 23:02:27 +01:00
|
|
|
webhook_token: str
|
|
|
|
|
|
|
|
|
|
|
|
TEMPLATES = {
|
|
|
|
"task.create": (
|
|
|
|
"<b>[{{ event_data.task.project_name }}]</b> "
|
|
|
|
"<font color='{{ event_data.task.color_id }}'>"
|
|
|
|
"{{ event_data.task.title }}"
|
|
|
|
"</font> "
|
|
|
|
"(#{{ event_data.task.id }}): "
|
|
|
|
"{{ event_author }} created the task in "
|
|
|
|
"'{{ event_data.task.column_title }}'\n"
|
|
|
|
"<blockquote>"
|
|
|
|
"{{ event_data.task.description | markdown }}"
|
|
|
|
"</blockquote>"
|
|
|
|
),
|
|
|
|
"task.move.column": (
|
|
|
|
"<b>[{{ event_data.task.project_name }}]</b> "
|
|
|
|
"<font color='{{ event_data.task.color_id }}'>"
|
|
|
|
"{{ event_data.task.title }}"
|
|
|
|
"</font> "
|
|
|
|
"(#{{ event_data.task.id }}): "
|
|
|
|
"{{ event_author }} moved the task to "
|
|
|
|
"'{{ event_data.task.column_title }}'"
|
|
|
|
),
|
|
|
|
"task.close": (
|
|
|
|
"<b>[{{ event_data.task.project_name }}]</b> "
|
|
|
|
"<font color='{{ event_data.task.color_id }}'>"
|
|
|
|
"{{ event_data.task.title }}"
|
|
|
|
"</font>"
|
|
|
|
"(#{{ event_data.task.id }}): "
|
|
|
|
"{{ event_author }} closed the task"
|
|
|
|
),
|
|
|
|
"comment.create": (
|
|
|
|
"<b>[{{ event_data.task.project_name }}]</b> "
|
|
|
|
"<font color='{{ event_data.task.color_id }}'>"
|
|
|
|
"{{ event_data.task.title }}"
|
|
|
|
"</font> "
|
|
|
|
"(#{{ event_data.task.id }}): "
|
|
|
|
"{{ event_author }} added a comment:\n"
|
|
|
|
"<blockquote>"
|
|
|
|
"{{ event_data.comment.comment | markdown }}"
|
|
|
|
"</blockquote>"
|
|
|
|
),
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async def format_events(config, src, dest):
|
|
|
|
|
|
|
|
env = jinja2.Environment()
|
|
|
|
md = markdown.Markdown(extensions=["fenced_code"])
|
|
|
|
env.filters["markdown"] = lambda src: jinja2.Markup(md.convert(src))
|
|
|
|
|
|
|
|
while True:
|
|
|
|
event = await src.get()
|
2021-03-15 23:20:54 +01:00
|
|
|
logging.debug("Formatting message: %s", event)
|
2021-03-15 23:59:36 +01:00
|
|
|
|
|
|
|
try:
|
|
|
|
project = event["event_data"]["project_id"]
|
|
|
|
except KeyError:
|
|
|
|
logging.warning("Missing project ID: %s", event)
|
|
|
|
continue
|
|
|
|
|
|
|
|
try:
|
|
|
|
room = config.matrix_rooms[project]
|
|
|
|
except KeyError:
|
|
|
|
logging.info("Unknown project ID: %d", project)
|
|
|
|
continue
|
|
|
|
|
2021-03-15 23:02:27 +01:00
|
|
|
try:
|
|
|
|
template = env.from_string(TEMPLATES[event["event_name"]])
|
|
|
|
except KeyError:
|
2021-03-15 23:20:54 +01:00
|
|
|
logging.info("Unknown message type: %s", event["event_name"])
|
2021-03-15 23:02:27 +01:00
|
|
|
continue
|
|
|
|
|
|
|
|
rendered = template.render(**event)
|
2021-03-15 23:59:36 +01:00
|
|
|
await dest.put((room, rendered))
|
2021-03-15 23:02:27 +01:00
|
|
|
|
|
|
|
|
|
|
|
async def send_notices(config, src):
|
|
|
|
client = nio.AsyncClient(config.matrix_homeserver, config.matrix_user)
|
|
|
|
await client.login(config.matrix_password)
|
|
|
|
|
|
|
|
while True:
|
2021-03-15 23:59:36 +01:00
|
|
|
room, formatted = await src.get()
|
2021-03-15 23:02:27 +01:00
|
|
|
|
|
|
|
await client.room_send(
|
2021-03-15 23:59:36 +01:00
|
|
|
room,
|
2021-03-15 23:02:27 +01:00
|
|
|
message_type="m.room.message",
|
|
|
|
content={
|
|
|
|
"msgtype": "m.notice",
|
|
|
|
"format": "org.matrix.custom.html",
|
|
|
|
"body": formatted, # à corriger
|
|
|
|
"formatted_body": formatted,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-03-16 00:57:37 +01:00
|
|
|
async def run_app(config, dest, host, port):
|
2021-03-15 23:02:27 +01:00
|
|
|
async def webhook(request):
|
|
|
|
tokens = request.query.getall("token", [])
|
|
|
|
if config.webhook_token not in tokens:
|
2021-03-15 23:20:54 +01:00
|
|
|
logging.warning("Invalid tokens: %s", tokens)
|
|
|
|
raise aiohttp.web.HTTPForbidden("Invalid tokens")
|
2021-03-15 23:02:27 +01:00
|
|
|
event = await request.json()
|
|
|
|
await dest.put(event)
|
|
|
|
|
|
|
|
app = aiohttp.web.Application()
|
|
|
|
app.add_routes([aiohttp.web.post("/webhook", webhook)])
|
|
|
|
|
|
|
|
runner = aiohttp.web.AppRunner(app)
|
|
|
|
await runner.setup()
|
2021-03-16 00:57:37 +01:00
|
|
|
await aiohttp.web.TCPSite(runner, host, port).start()
|
2021-03-15 23:02:27 +01:00
|
|
|
|
|
|
|
|
|
|
|
async def main():
|
|
|
|
|
2021-03-15 23:20:54 +01:00
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
2021-03-15 23:02:27 +01:00
|
|
|
parser = argparse.ArgumentParser()
|
2021-03-16 00:57:37 +01:00
|
|
|
parser.add_argument("-c", "--config", default="config.yaml")
|
|
|
|
parser.add_argument("--host", default="0.0.0.0")
|
|
|
|
parser.add_argument("--port", type=int, default=8000)
|
2021-03-15 23:02:27 +01:00
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
2021-03-16 00:57:37 +01:00
|
|
|
with open(args.config) as f:
|
2021-03-15 23:02:27 +01:00
|
|
|
config = yaml.safe_load(f)
|
|
|
|
|
|
|
|
config = Config(**config)
|
|
|
|
|
|
|
|
events = asyncio.Queue()
|
|
|
|
formatted = asyncio.Queue()
|
|
|
|
|
2021-03-16 00:57:37 +01:00
|
|
|
logging.info("Started Kanbot on %s:%d", args.host, args.port)
|
2021-03-15 23:20:54 +01:00
|
|
|
|
2021-03-15 23:02:27 +01:00
|
|
|
tasks = (
|
2021-03-16 00:57:37 +01:00
|
|
|
asyncio.create_task(run_app(config, events, args.host, args.port)),
|
2021-03-15 23:02:27 +01:00
|
|
|
asyncio.create_task(format_events(config, events, formatted)),
|
|
|
|
asyncio.create_task(send_notices(config, formatted)),
|
|
|
|
)
|
|
|
|
|
|
|
|
# on propage les exceptions
|
|
|
|
await asyncio.gather(*tasks, return_exceptions=False)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
asyncio.get_event_loop().run_until_complete(main())
|