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;
|
python3 = pkgs.python311;
|
||||||
python3Pkgs = python3.pkgs;
|
python3Pkgs = python3.pkgs;
|
||||||
ircrobots = python3Pkgs.callPackage ./ircrobots.nix { };
|
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
|
in
|
||||||
{
|
{
|
||||||
options.dgn-chatops = {
|
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
|
import asyncio
|
||||||
|
|
||||||
from irctokens import build, Line
|
from irctokens.line import build, Line
|
||||||
from ircrobots import Bot as BaseBot
|
from ircrobots.bot import Bot as BaseBot
|
||||||
from ircrobots import Server as BaseServer
|
from ircrobots.server import Server as BaseServer
|
||||||
from ircrobots import ConnectionParams
|
from ircrobots.params import ConnectionParams
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
BRIDGE_NICKNAME = "hermes"
|
||||||
|
|
||||||
SERVERS = [
|
SERVERS = [
|
||||||
("dgnum", "irc.dgnum.eu")
|
("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):
|
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):
|
async def line_read(self, line: Line):
|
||||||
print(f"{self.name} < {line.format()}")
|
print(f"{self.name} < {line.format()}")
|
||||||
if line.command == "001":
|
if line.command == "001":
|
||||||
print(f"connected to {self.isupport.network}")
|
print(f"connected to {self.isupport.network}")
|
||||||
await self.send(build("JOIN", ["#dgnum-bridge-test"]))
|
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):
|
async def line_send(self, line: Line):
|
||||||
print(f"{self.name} > {line.format()}")
|
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