feat(dgn-chatops): Takumi v1.0.0

Can schedule meet.dgnum.eu in the chat upon demand.

Signed-off-by: Ryan Lahfa <ryan@dgnum.eu>
This commit is contained in:
Ryan Lahfa 2024-09-20 23:38:58 +02:00
parent 4a275fd07e
commit 1c6124f376
7 changed files with 248 additions and 5 deletions

View file

@ -0,0 +1 @@
use nix

View file

@ -43,7 +43,12 @@ let
python3 = pkgs.python311;
python3Pkgs = python3.pkgs;
ircrobots = python3Pkgs.callPackage ./ircrobots.nix { };
ps = python3Pkgs.makePythonPath [ ircrobots ];
tortoise-orm = python3Pkgs.callPackage ./tortoise-orm.nix { };
ps = python3Pkgs.makePythonPath [
ircrobots
tortoise-orm
python3Pkgs.aiohttp
];
in
{
options.dgn-chatops = {

View file

@ -0,0 +1,31 @@
{
lib,
buildPythonPackage,
fetchFromGitHub,
poetry-core,
}:
buildPythonPackage rec {
pname = "pypika-tortoise";
version = "0.1.6";
pyproject = true;
src = fetchFromGitHub {
owner = "tortoise";
repo = "pypika-tortoise";
rev = "v${version}";
hash = "sha256-xx5FUMHh6413fsvwrEA+Q0tBmJWy00h5O6YijvrJyCE=";
};
build-system = [ poetry-core ];
pythonImportsCheck = [ "pypika" ];
meta = {
description = "";
homepage = "https://github.com/tortoise/pypika-tortoise";
changelog = "https://github.com/tortoise/pypika-tortoise/blob/${src.rev}/CHANGELOG.md";
license = lib.licenses.asl20;
maintainers = with lib.maintainers; [ raitobezarius ];
};
}

View file

@ -0,0 +1,20 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "takumi"
version = "1.0.0"
authors = [
{ name = "Ryan Lahfa", email = "ryan@dgnum.eu" },
]
description = "Fully automatic day-to-day operations at DGNum"
requires-python = ">=3.11"
classifiers = [
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
]
[project.urls]
Homepage = "https://git.dgnum.eu/DGNum/infrastructure"
Issues = "https://git.dgnum.eu/DGNum/infrastructure/issues"

View file

@ -0,0 +1,29 @@
{
pkgs ? import <nixpkgs> { },
python3 ? pkgs.python3,
}:
let
takumi = python3.pkgs.buildPythonPackage rec {
pname = "takumi";
version = "1.0.0";
pyproject = true;
src = ./.;
build-system = [ python3.pkgs.hatchling ];
dependencies = [
(python3.pkgs.callPackage ./ircrobots.nix { })
(python3.pkgs.callPackage ./tortoise-orm.nix { })
python3.pkgs.aiohttp
];
postInstall = ''
mkdir -p $out/bin
cp -v takumi.py $out/bin/takumi.py
chmod +x $out/bin/takumi.py
wrapProgram $out/bin/takumi.py --prefix PYTHONPATH : "$PYTHONPATH"
'';
};
in
pkgs.mkShell { packages = [ takumi ]; }

View file

@ -1,20 +1,106 @@
#!/usr/bin/env python3
import asyncio
from irctokens import build, Line
from ircrobots import Bot as BaseBot
from ircrobots import Server as BaseServer
from ircrobots import ConnectionParams
from irctokens.line import build, Line
from ircrobots.bot import Bot as BaseBot
from ircrobots.server import Server as BaseServer
from ircrobots.params import ConnectionParams
import aiohttp
BRIDGE_NICKNAME = "hermes"
SERVERS = [
("dgnum", "irc.dgnum.eu")
]
TEAMS = {
"fai": ("tomate", "elias", "JeMaGius", "Luj", "catvayor", "Raito"),
"marketing": ("cst1", "elias"),
"bureau": ("Raito", "JeMaGius", "Luj", "gdd")
}
# times format is 0700-29092024
TRIGGER = '!'
async def create_meet(title: str, times: list[str], timezone: str = "UTC") -> str:
async with aiohttp.ClientSession() as session:
payload = {
'name': title,
'times': times,
'timezone': timezone
}
async with session.post('https://api.meet.dgnum.eu/event', json=payload) as response:
response.raise_for_status()
id = (await response.json()).get('id')
if not id:
raise RuntimeError('No ID attributed to a meet')
return f'https://meet.dgnum.eu/{id}'
def expand_times(times: list[str]) -> list[str]:
expanded = []
# TODO: verify the date exist in the calendar
# TODO: verify that we don't write any duplicates.
for time in times:
if '-' not in time:
for i in range(7, 20):
expanded.append(f'{i:02}00-{time}')
else:
expanded.append(time)
return expanded
def bridge_stripped(possible_command: str, origin_nick: str) -> str | None:
if origin_nick.lower() == BRIDGE_NICKNAME:
stripped_user = possible_command.split(':')[1].lstrip()
return stripped_user if stripped_user.startswith(TRIGGER) else None
else:
return possible_command if possible_command.startswith(TRIGGER) else None
class Server(BaseServer):
def extract_valid_command(self, line: Line) -> str | None:
me = self.nickname_lower
if line.command == "PRIVMSG" and \
self.has_channel(line.params[0]) and \
line.hostmask is not None and \
self.casefold(line.hostmask.nickname) != me and \
self.has_user(line.hostmask.nickname):
return bridge_stripped(line.params[1], line.hostmask.nickname)
async def line_read(self, line: Line):
print(f"{self.name} < {line.format()}")
if line.command == "001":
print(f"connected to {self.isupport.network}")
await self.send(build("JOIN", ["#dgnum-bridge-test"]))
# In case `!probe_meet <title> <team> <time_1> <time_2> … <time_N> [<timezone>]`
if (command := self.extract_valid_command(line)) is not None:
text = command.lstrip(TRIGGER)
if text.startswith('probe_meet'):
args = text.split(' ')
if len(args) < 4:
await self.send(build("PRIVMSG", [line.params[0], "usage is !probe_meet <title> <team> <time_1> [<time_2> <time_3> … <time_N>] ; time is in [00-hour-]DDMMYYYY format."]))
return
title, team = args[1], args[2]
print(f"creating meet '{title}' for team '{team}'")
try:
times = expand_times(args[3:])
link = await create_meet(title, times)
if team not in TEAMS:
await self.send(build("PRIVMSG", [line.params[0], f"team {team} does not exist"]))
return
targets = TEAMS[team]
ping_mentions = ', '.join(targets)
await self.send(build("PRIVMSG", [line.params[0], f'{ping_mentions} {link}']))
except ValueError as e:
print(e)
await self.send(build("PRIVMSG", [line.params[0], "time format is [00-hour-]DDMMYYYY, hour is optional, by default it's 07:00 to 19:00 in Europe/Paris timezone"]))
except aiohttp.ClientError as e:
print(e)
await self.send(build("PRIVMSG", [line.params[0], "failed to create the meet on meet.dgnum.eu, API error, check the logs"]))
async def line_send(self, line: Line):
print(f"{self.name} > {line.format()}")

View file

@ -0,0 +1,71 @@
{
lib,
buildPythonPackage,
fetchFromGitHub,
poetry-core,
aiosqlite,
iso8601,
callPackage,
pytz,
ciso8601,
orjson,
uvloop,
aiomysql,
asyncmy,
asyncpg,
psycopg,
pydantic,
pythonRelaxDepsHook,
}:
buildPythonPackage rec {
pname = "tortoise-orm";
version = "0.21.6";
pyproject = true;
src = fetchFromGitHub {
owner = "tortoise";
repo = "tortoise-orm";
rev = version;
hash = "sha256-Gu7MSJbPjaGUN6tmHwkmx7Bdy/+V1wZjmTCQrTDDPkw=";
};
buildInputs = [ pythonRelaxDepsHook ];
pythonRelaxDeps = [
"aiosqlite"
"iso8601"
];
build-system = [ poetry-core ];
dependencies = [
aiosqlite
iso8601
pydantic
(callPackage ./pypika-tortoise.nix { })
pytz
];
optional-dependencies = {
accel = [
ciso8601
orjson
uvloop
];
aiomysql = [ aiomysql ];
asyncmy = [ asyncmy ];
asyncpg = [ asyncpg ];
psycopg = [ psycopg ];
};
pythonImportsCheck = [ "tortoise" ];
meta = {
description = "";
homepage = "https://github.com/tortoise/tortoise-orm";
changelog = "https://github.com/tortoise/tortoise-orm/blob/${src.rev}/CHANGELOG.rst";
license = lib.licenses.asl20;
maintainers = with lib.maintainers; [ raitobezarius ];
};
}