diff --git a/Readme.md b/Readme.md index 35c447d..ececd35 100644 --- a/Readme.md +++ b/Readme.md @@ -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 + 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. ## 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. ## 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. 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). @@ -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) ## 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) POST /id -> checks the item -> returns same as GET / ## parent server facing interface specification + GET -> find a way to get the list of VTODO on the specified calendar. POST -> find post the updated item ## internal logic + Filter requests by due date and truncate them. Update an item with respecting repeating events parse to csv update list according to csv conflict management policy - diff --git a/default_config.ini b/default_config.ini deleted file mode 100644 index 80515ae..0000000 --- a/default_config.ini +++ /dev/null @@ -1,8 +0,0 @@ -[calDAV] -address = localhost:5232 -username = username -password = password -passwordFile = /path/to/password -[server] -port = 8000 - diff --git a/flake.nix b/flake.nix index 0399941..36277bd 100644 --- a/flake.nix +++ b/flake.nix @@ -7,138 +7,102 @@ "github:nixos/nixpkgs/nixos-unstable"; # Specify the version or channel as needed }; - outputs = { self, nixpkgs, ... }: { - packages.x86_64-linux.calDavToCsv = - nixpkgs.python3.pkgs.buildPythonApplication rec { - 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, ... }: - let cfg = config.services.calDavToCsv; - in { - options.services.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 { - type = lib.types.str; - default = 8000; - example = "6000"; - description = - "Port of the server, that will need to be reverse proxied into"; - }; - }; - }; - }; - # 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 { - environment.etc."config.ini".text = '' - ${lib.toIni cfg.settings} + outputs = { self, nixpkgs, ... }: + let pkgs = nixpkgs.legacyPackages.x86_64-linux; + in { + nixosModules.calDavToCsv = { config, lib, pkgs, ... }: + 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 ''; - systemd.services.calDavToCsv = { - description = "calDAV to CSV Service"; - after = [ "network.target" ]; - serviceConfig = { - ExecStart = - "${pkgs.python3.pkgs.gunicorn}/bin/gunicorn -w 1 -b 0.0.0.0:${cfg.settings.server.port} main:app"; - Restart = "on-failure"; + 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 { + options.services.calDavToCsv = { + enable = lib.mkEnableOption "calDavToCsv"; + port = lib.mkOption { + type = lib.types.int; + default = 8000; + 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 = + "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.mkIf cfg.enable { + environment.etc."calDAVtoCSV/config.ini".text = '' + [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 = { + description = + "calDAV to CSV Service used as a middleman between my esp32 and my caldav server"; + after = [ "network.target" ]; + serviceConfig = { + ExecStart = "${run-with-wsgi}/bin/run-app"; + Restart = "on-failure"; + }; + wantedBy = [ "multi-user.target" ]; }; - wantedBy = [ "multi-user.target" ]; }; }; - }; - }; + }; } diff --git a/main.py b/main.py index 538d861..39a2daf 100755 --- a/main.py +++ b/main.py @@ -5,43 +5,32 @@ from caldav import DAVClient, Calendar, Principal, Todo 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 passwordFromFile() -> str: + calDavPasswordFile = config.get("calDAV", "passwordFile") + with open(calDavPasswordFile, "r") as file: + return file.readline().strip() + config = configparser.ConfigParser() - config.read("/home/stuce/calDAVtoCSV/config.ini") # TODO: change this - global calDavAddress - global calDavUsername - global calDavPassword - global calDavCalendarName - global serverPort + config.read("/etc/calDAVtoCSV/config.ini") calDavAddress = config.get("calDAV", "address") + print(calDavAddress) calDavUsername = config.get("calDAV", "username") - calDavPassword = config.get("calDAV", "password") - calDavPasswordFile = config.get("calDAV", "passwordFile") + caldavpwd = config.get("calDAV", "password", fallback=None) + caldavpwd = caldavpwd if caldavpwd is not None else passwordFromFile() calDavCalendarName = config.get("calDAV", "calendarName") - serverPort = config.getint("server", "port") + return calDavAddress, calDavUsername, caldavpwd, calDavCalendarName 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 - client = DAVClient( - url=calDavAddress, username=calDavUsername, password=calDavPassword - ) + client = DAVClient(url=add, username=user, password=pwd) principal = client.principal() return principal - def get_calendar(principal: Principal) -> Calendar: - calendar = principal.calendar(name="Ouais le ménage") + def get_calendar(principal: Principal, name: str) -> Calendar: + calendar = principal.calendar(name=name) return calendar def todos_to_csv(todos: list[Todo]) -> str: @@ -51,8 +40,9 @@ def fetch_10_next_todos_as_csv() -> str: ) return result - principal = auth() - calendar = get_calendar(principal) + add, user, pwd, name = parseConfig() + principal = auth(add, user, pwd) + calendar = get_calendar(principal, name) sorted_todos = calendar.todos(sort_keys=("due")) cut_todos = sorted_todos[:10] @@ -73,5 +63,4 @@ def send_events(): if __name__ == "__main__": - parseConfig() - app.run(host="0.0.0.0", port=serverPort) + app.run(host="0.0.0.0", port=8000) diff --git a/setup.py b/setup.py deleted file mode 100644 index bb208fa..0000000 --- a/setup.py +++ /dev/null @@ -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"], -)