diff --git a/.gitignore b/.gitignore index a054264..72cc436 100644 --- a/.gitignore +++ b/.gitignore @@ -132,3 +132,7 @@ dmypy.json .pyre/ app/config/calendar.json /app/cache/ + +# Development directories +/app/config/ +.vscode diff --git a/README.md b/README.md index 259cab8..3570bcd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # ICS Fusion +Forked from [https://github.com/jdejaegh/ics-fusion](https://github.com/jdejaegh/ics-fusion) ## Introduction ICS Fusion is a tool to merge multiple ics feed into a single ics calendar. Filters and modifications may be applied on the incoming feeds. The resulting ics can be accessed via an HTTP endpoint. @@ -30,6 +31,11 @@ The JSON configuration file should look like the following. ```json [ + { + "conf": true, + "extends": "str", + "extendFail": "str", + }, { "url":"str", "name":"str", @@ -59,15 +65,18 @@ The JSON configuration file should look like the following. }, "name":{ "addPrefix":"str", - "addSuffix":"str" + "addSuffix":"str", + "redactAs":"str" }, "description":{ "addPrefix":"str", - "addSuffix":"str" + "addSuffix":"str", + "redactAs":"str" }, "location":{ "addPrefix":"str", - "addSuffix":"str" + "addSuffix":"str", + "redactAs":"str" } } } @@ -98,11 +107,23 @@ Only the `url` and the `name` field are mandatory. - `location`: modification to apply to the location of the events - `addPrefix`: string to add at the beginning of the field - `addSuffix`: string to add at the end of the field +- `redactAs`: Replaces the content of the field with the specified string If multiple calendars are specified in the configuration list, their events will be merged in the resulting ics feed. +The first dataset with {"conf": "true",} specifies options that are globally applied to all calenders in the conf. Omit this set to disable. Options +- `extends`: string specifying the name (excluding .json) of another config file to extend. +- `extendFail`: string speciying the action to take if an extend fails, either "fail" or "ignore". Default is "fail". ## Usage Once the config file is created, the corresponding HTTP endpoint is accessible. For example, if the file `app/config/my-calendar.json` contains the configuration, the HTTP endpoint will be `http://localhost:8088/my-calendar`. +A config can extend another config file, to do this the extended config should contain begin with`{ + "conf": true, + "extends": , + "extendFail": "fail", + },` +For an extend to work calendars MUST share the same name between the configs +An extending config cannot remove data from a base calendar but can modify fields + ## Limitations Currently, the application only merges events of the ics feeds, the alarms and todos are not supported. diff --git a/app/tools/tools.py b/app/tools/tools.py index e0aa8e2..c9f6154 100644 --- a/app/tools/tools.py +++ b/app/tools/tools.py @@ -34,15 +34,18 @@ The JSON configuration file used in this module has the following structure }, "name":{ "addPrefix":"str", - "addSuffix":"str" + "addSuffix":"str", + "redactAs":"str" }, "description":{ "addPrefix":"str", - "addSuffix":"str" + "addSuffix":"str", + "redactAs":"str" }, "location":{ "addPrefix":"str", - "addSuffix":"str" + "addSuffix":"str", + "redactAs":"str" } } } @@ -69,6 +72,7 @@ Only the url and the name field are mandatory. - location: modification to apply to the location of the events - addPrefix: string to add at the beginning of the field - addSuffix: string to add at the end of the field +- redactAs: string to replace the field with """ import json @@ -192,9 +196,16 @@ def modify_time(cal: Calendar, modify: dict) -> Calendar: hour = 0 if not ("hour" in shift) else shift["hour"] minute = 0 if not ("minute" in shift) else shift["minute"] - for event in cal.events: - event.end = event.end.shift(years=year, months=month, days=day, hours=hour, minutes=minute) - event.begin = event.begin.shift(years=year, months=month, days=day, hours=hour, minutes=minute) + shift_minutes = (year * 365 * 24 * 60) + (month * 30 * 24 * 60) + (day * 24 * 60) + (hour * 60) + minute + + if shift_minutes > 0: + for event in cal.events: + event.end = event.end.shift(minutes=shift_minutes) + event.begin = event.begin.shift(minutes=shift_minutes) + elif shift_minutes < 0: + for event in cal.events: + event.begin = event.begin.shift(minutes=shift_minutes) + event.end = event.end.shift(minutes=shift_minutes) return cal @@ -250,6 +261,18 @@ def modify_text(cal: Calendar, modify: dict, field_name: str) -> Calendar: elif field_name == "location": event.location = event.location + change["addSuffix"] \ if event.location is not None else change["addSuffix"] + + if "redactAs" in change: + for event in cal.events: + + if field_name == "name": + event.name = change["redactAs"] + + elif field_name == "description": + event.description = change["redactAs"] + + elif field_name == "location": + event.location = change["redactAs"] return cal @@ -330,9 +353,31 @@ def process(path: str, from_cache: bool = True) -> Calendar: file.close() data = [] + + for entry in config: + if "conf" in entry: + if entry.get("extends", None) is not None: + try: + o = "app/config/" + sanitize_filename(entry["extends"]) + ".json" + print("Try to open " + o) + file = open(o, "r") + baseConfig = json.loads(file.read()) + file.close() + extendingConfig = config + try: + config = merge_json(baseConfig, extendingConfig) + except: + config = extendingConfig + + except: + if entry.get("extendFail", "fail") == "fail": + raise FileNotFoundError("The calendar is not cached") + else: + pass + - for entry in config: - + for entry in config: + if "conf" not in entry: cal = load_cal(entry) if "filters" in entry: @@ -343,7 +388,7 @@ def process(path: str, from_cache: bool = True) -> Calendar: data.append(cal) - return merge(data) + return merge(data) def get_from_cache(entry: dict) -> Calendar: @@ -406,7 +451,7 @@ def load_cal(entry: dict) -> Calendar: else: cal = Calendar(imports=r.content.decode()) - cal = horodate(cal, 'Downloaded at') + cal = horodate(cal, 'Event last fetched: ') return cal @@ -432,3 +477,34 @@ def horodate(cal: Calendar, prefix='') -> Calendar: if event.description is not None else prefix + ' ' + now return cal + +def merge_json(base, extension): + """Merges two config files by updating the value of base with the values in extension. + + :param base: the base config file + :type base: dict + + :param extension: the config file to merge with the base + :type extension: dict + + :return: the merged config file + :rtype: dict + """ + + new_json = base.copy() + + def update_json(base_set, updates): + for key, value in updates.items(): + if not key == "conf": + if isinstance(value, dict) and key in base_set and isinstance(base_set[key], dict): + update_json(base_set[key], value) + else: + base_set[key] = value + + for base_dataset in new_json: + if "conf" not in base_dataset: + for ext_dataset in extension: + if base_dataset.get("name") == ext_dataset.get("name"): + update_json(base_dataset, ext_dataset) + + return new_json