forked from DGNum/infrastructure
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:
parent
4a275fd07e
commit
1c6124f376
7 changed files with 248 additions and 5 deletions
1
modules/dgn-chatops/.envrc
Normal file
1
modules/dgn-chatops/.envrc
Normal file
|
@ -0,0 +1 @@
|
|||
use nix
|
|
@ -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 = {
|
||||
|
|
31
modules/dgn-chatops/pypika-tortoise.nix
Normal file
31
modules/dgn-chatops/pypika-tortoise.nix
Normal 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 ];
|
||||
};
|
||||
}
|
20
modules/dgn-chatops/pyproject.toml
Normal file
20
modules/dgn-chatops/pyproject.toml
Normal 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"
|
29
modules/dgn-chatops/shell.nix
Normal file
29
modules/dgn-chatops/shell.nix
Normal 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 ]; }
|
|
@ -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()}")
|
||||
|
||||
|
|
71
modules/dgn-chatops/tortoise-orm.nix
Normal file
71
modules/dgn-chatops/tortoise-orm.nix
Normal 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 ];
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue