This commit is contained in:
Stuce 2025-11-17 17:42:26 +01:00
parent cc1950aa94
commit f1ff6afdc8
5 changed files with 130 additions and 180 deletions

View file

@ -1,11 +1,26 @@
## TODO
- [x] settings file to not have hardcoded credentials
- [ ] change production credentials
- [x] nix flake with complete module
- [ ] post behavior (simple)
- [ ] post behavior (concurrent protection)
- [ ] check if better http server availible or fastcgi better suited
- [ ] parse args to not hardcode config location
- [ ] cleanup nix flake
- [ ] de-duplicate hard coded strings in flake
## Goals ## Goals
The goal is to make a service between a m5paper and my radicale (caldav) server. The goal is to make a service between a m5paper and my radicale (caldav) server.
The service will query the caldav server and modify the data in an easy to digest csv that can be sent back and forth without issues. The service will query the caldav server and modify the data in an easy to digest csv that can be sent back and forth without issues.
## Problems to solve ## Problems to solve
Caldav format too hard to parse. Prefer to use a high level library. High level library can't run on esp32. Need a third party. Caldav format too hard to parse. Prefer to use a high level library. High level library can't run on esp32. Need a third party.
## Justification of choices ## Justification of choices
The time invested in a proper caldav format parser would take too much time reading a specification, and not enough enjoying/learning programming. The time invested in a proper caldav format parser would take too much time reading a specification, and not enough enjoying/learning programming.
An intermediary could prove interesting as I want to use a self-signed cert to authenticate, which I never did (everything goes trough ldap or authelia behind a reverse proxy atm). An intermediary could prove interesting as I want to use a self-signed cert to authenticate, which I never did (everything goes trough ldap or authelia behind a reverse proxy atm).
It also forces me to learn enough about caldav to be interesting yet not cumbersome. (The rfc doc is very long, and does feel like a chore to read). It also forces me to learn enough about caldav to be interesting yet not cumbersome. (The rfc doc is very long, and does feel like a chore to read).
@ -17,17 +32,19 @@ Allows a call to fetch the specified caldav (specified in the config, not via a
Nix will be used as it is planned to be deployed on nixos like the rest of my web infrastructure (simply the best to not get overwhelmed by the amount of tweaking I do) Nix will be used as it is planned to be deployed on nixos like the rest of my web infrastructure (simply the best to not get overwhelmed by the amount of tweaking I do)
## client facing interface specification ## client facing interface specification
GET / -> gets 10 next tasks of calendar data as a csv containing an id, a boolean value (0 or 1), and a string (terminated by a \n, no carriage return windows bullshit) GET / -> gets 10 next tasks of calendar data as a csv containing an id, a boolean value (0 or 1), and a string (terminated by a \n, no carriage return windows bullshit)
POST /id -> checks the item -> returns same as GET / POST /id -> checks the item -> returns same as GET /
## parent server facing interface specification ## parent server facing interface specification
GET -> find a way to get the list of VTODO on the specified calendar. GET -> find a way to get the list of VTODO on the specified calendar.
POST -> find post the updated item POST -> find post the updated item
## internal logic ## internal logic
Filter requests by due date and truncate them. Filter requests by due date and truncate them.
Update an item with respecting repeating events Update an item with respecting repeating events
parse to csv parse to csv
update list according to csv update list according to csv
conflict management policy conflict management policy

View file

@ -1,8 +0,0 @@
[calDAV]
address = localhost:5232
username = username
password = password
passwordFile = /path/to/password
[server]
port = 8000

190
flake.nix
View file

@ -7,133 +7,97 @@
"github:nixos/nixpkgs/nixos-unstable"; # Specify the version or channel as needed "github:nixos/nixpkgs/nixos-unstable"; # Specify the version or channel as needed
}; };
outputs = { self, nixpkgs, ... }: { outputs = { self, nixpkgs, ... }:
packages.x86_64-linux.calDavToCsv = let pkgs = nixpkgs.legacyPackages.x86_64-linux;
nixpkgs.python3.pkgs.buildPythonApplication rec { in {
pname = "calDAVtoCSV";
version = "0.1.0";
pyproject = true;
src = builtins.fetchGit {
url = "git+https://git.stuce.ch/Stuce/calDAVtoCSV.git";
# rev = "ca6bdb889085893cd4494cd1612a00f8e164ffac";
};
build-system = with nixpkgs.python3.pkgs; [ setuptools ];
dependencies = with nixpkgs.python3.pkgs; [ caldav flask gunicorn ];
meta = {
description =
"middleman program between my radicale server and my esp32";
license = nixpkgs.lib.licenses.mit;
maintainers = with nixpkgs.lib.maintainers; [ stuce-bot ];
};
};
nixosModules.calDavToCsv = { config, lib, pkgs, ... }: nixosModules.calDavToCsv = { config, lib, pkgs, ... }:
let cfg = config.services.calDavToCsv; let
flaskApp = ./main.py;
cfg = config.services.calDavToCsv;
python-env = pkgs.python3.withPackages (ps: [ ps.flask ps.caldav ]);
app_ini = pkgs.writeText "api.ini" ''
[uwsgi]
wsgi-file = ${flaskApp}
callable = app
http = :${toString cfg.port}
processes = 1
threads = 1
master = true
chmod-socket = 660
vacuum = true
plugins = python3
die-on-term = true
uid = calDAVtoCSV
gid = calDAVtoCSV
'';
uwsgi = pkgs.uwsgi.override {
python3 = python-env;
plugins = [ "python3" ];
};
run-with-wsgi = pkgs.writeShellApplication {
name = "run-app";
text = ''
export PYTHONPATH="${python-env}/lib/python${
builtins.substring 0 4 python-env.python.version
}/site-packages"
${uwsgi}/bin/uwsgi --ini ${app_ini}
'';
};
in { in {
options.services.calDavToCsv = { options.services.calDavToCsv = {
enable = lib.mkEnableOption "calDavToCsv"; enable = lib.mkEnableOption "calDavToCsv";
settings = lib.mkOption {
description = ''
Your config as an attribute set
'';
default = { };
#TODO: provide example
example = ''
{
}
'';
type = lib.types.submodule {
# freeformType = lib.format.type;
caldav = {
address = lib.mkOption {
type = lib.types.str;
default = "tcp://:5232/";
example =
"unix:///var/run/authelia.sock?path=authelia&umask=0117";
description = "Address of the caldav calendar";
};
username = lib.mkOption {
type = lib.types.str;
example = "John-doe";
description = "Username of the caldav calendar";
};
password = lib.mkOption {
type = lib.types.str;
example = "unsafe-password";
description =
"Password of the caldav calendar, NOT RECOMMENDED, use passwordFile instead !";
};
passwordFile = lib.mkOption {
type = lib.types.str;
example = "/var/lib/calDavToCsv/caldavPassword";
description =
"Password of the caldav calendar, NOT RECOMMENDED, use passwordFile instead !";
};
};
server = {
port = lib.mkOption { port = lib.mkOption {
type = lib.types.str; type = lib.types.int;
default = 8000; default = 8000;
example = "6000"; description = "Port on which calDAVtoCSV will listen";
};
calendarUrl = lib.mkOption {
type = lib.types.str;
description = "url of the calendar";
};
calendarUsername = lib.mkOption {
type = lib.types.str;
description = "username of the calendar account";
};
calendarName = lib.mkOption {
type = lib.types.str;
description = description =
"Port of the server, that will need to be reverse proxied into"; "name of the calendar we will fetch the todo items from";
};
calendarPasswordFile = lib.mkOption {
type = lib.types.str;
description =
"file where we need to look for password to connect, needs to be readeable by the service user";
};
package = lib.mkOption {
type = lib.types.package;
default = self.packages.x86_64-linux.calDavToCsv;
}; };
}; };
};
};
# config = lib.mkOption {
# type = lib.types.attrs;
# calDavToCsv = {
# calDAV = {
# address = "localhost:5232";
# username = "username";
# password = "password";
# passwordFile = "/path/to/password";
# };
# server = { port = 8000; };
# };
# description = "User-defined configuration for the service";
# };
};
# options.NixosModule = {
# enable = lib.mkEnableOption "Enable calDAVtoCSV service";
# port = lib.mkOption {
# type = lib.types.int;
# calDavToCsv = 8000;
# description = "Port on which calDAVtoCSV will listen";
# };
# url = lib.mkOption {
# type = lib.types.str;
# description = "url of the calendar";
# };
# calendarUsername = lib.mkOption {
# type = lib.types.str;
# description = "username of the calendar account";
# };
# calendarName = lib.mkOption {
# type = lib.types.str;
# description =
# "name of the calendar we will fetch the todo items from";
# };
# calendarPasswordFile = lib.mkOption {
# type = lib.types.str;
# description =
# "file where we need to look for password to connect, needs to be readeable by the service user";
# };
# };
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
environment.etc."config.ini".text = '' environment.etc."calDAVtoCSV/config.ini".text = ''
${lib.toIni cfg.settings} [calDAV]
address = ${cfg.calendarUrl}
username = ${cfg.calendarUsername}
calendarName = ${cfg.calendarName}
passwordFile = ${cfg.calendarPasswordFile}
[server]
port = ${toString cfg.port}
''; '';
users.groups."calDAVtoCSV".name = "calDAVtoCSV";
users.users."calDAVtoCSV" = {
name = "calDAVtoCSV";
isSystemUser = true;
group = "calDAVtoCSV";
};
systemd.services.calDavToCsv = { systemd.services.calDavToCsv = {
description = "calDAV to CSV Service"; description =
"calDAV to CSV Service used as a middleman between my esp32 and my caldav server";
after = [ "network.target" ]; after = [ "network.target" ];
serviceConfig = { serviceConfig = {
ExecStart = ExecStart = "${run-with-wsgi}/bin/run-app";
"${pkgs.python3.pkgs.gunicorn}/bin/gunicorn -w 1 -b 0.0.0.0:${cfg.settings.server.port} main:app";
Restart = "on-failure"; Restart = "on-failure";
}; };
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];

51
main.py
View file

@ -5,43 +5,32 @@ from caldav import DAVClient, Calendar, Principal, Todo
import configparser import configparser
# TODO:
# [x] settings file to not have hardcoded credentials
# [ ] change production credentials
# [ ] nix flake with complete module
# [ ] post behavior (simple)
# [ ] post behavior (concurrent protection)
# [ ] check if better http server availible or fastcgi better suited
#!/usr/bin/env python
def parseConfig(): def parseConfig():
config = configparser.ConfigParser() def passwordFromFile() -> str:
config.read("/home/stuce/calDAVtoCSV/config.ini") # TODO: change this
global calDavAddress
global calDavUsername
global calDavPassword
global calDavCalendarName
global serverPort
calDavAddress = config.get("calDAV", "address")
calDavUsername = config.get("calDAV", "username")
calDavPassword = config.get("calDAV", "password")
calDavPasswordFile = config.get("calDAV", "passwordFile") calDavPasswordFile = config.get("calDAV", "passwordFile")
with open(calDavPasswordFile, "r") as file:
return file.readline().strip()
config = configparser.ConfigParser()
config.read("/etc/calDAVtoCSV/config.ini")
calDavAddress = config.get("calDAV", "address")
print(calDavAddress)
calDavUsername = config.get("calDAV", "username")
caldavpwd = config.get("calDAV", "password", fallback=None)
caldavpwd = caldavpwd if caldavpwd is not None else passwordFromFile()
calDavCalendarName = config.get("calDAV", "calendarName") calDavCalendarName = config.get("calDAV", "calendarName")
serverPort = config.getint("server", "port") return calDavAddress, calDavUsername, caldavpwd, calDavCalendarName
def fetch_10_next_todos_as_csv() -> str: def fetch_10_next_todos_as_csv() -> str:
def auth() -> Principal: def auth(add: str, user: str, pwd: str) -> Principal:
# TODO: on the final version, fetch it locally # TODO: on the final version, fetch it locally
client = DAVClient( client = DAVClient(url=add, username=user, password=pwd)
url=calDavAddress, username=calDavUsername, password=calDavPassword
)
principal = client.principal() principal = client.principal()
return principal return principal
def get_calendar(principal: Principal) -> Calendar: def get_calendar(principal: Principal, name: str) -> Calendar:
calendar = principal.calendar(name="Ouais le ménage") calendar = principal.calendar(name=name)
return calendar return calendar
def todos_to_csv(todos: list[Todo]) -> str: def todos_to_csv(todos: list[Todo]) -> str:
@ -51,8 +40,9 @@ def fetch_10_next_todos_as_csv() -> str:
) )
return result return result
principal = auth() add, user, pwd, name = parseConfig()
calendar = get_calendar(principal) principal = auth(add, user, pwd)
calendar = get_calendar(principal, name)
sorted_todos = calendar.todos(sort_keys=("due")) sorted_todos = calendar.todos(sort_keys=("due"))
cut_todos = sorted_todos[:10] cut_todos = sorted_todos[:10]
@ -73,5 +63,4 @@ def send_events():
if __name__ == "__main__": if __name__ == "__main__":
parseConfig() app.run(host="0.0.0.0", port=8000)
app.run(host="0.0.0.0", port=serverPort)

View file

@ -1,12 +0,0 @@
#!/usr/bin/env python
from setuptools import setup, find_packages
setup(
name="calDAVtoCSV",
version="1.0",
# Modules to import from other scripts:
packages=find_packages(),
# Executables
scripts=["main.py"],
)