diff --git a/run_tests.sh b/run_tests.sh index e50dea1..3aa0ad9 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -5,7 +5,7 @@ if [ $version ] then versions=("$version") else - versions=(1.22.1 1.22.0 1.21.3) + versions=(1.23.1 1.23.0 1.22.1 1.22.0 1.21.3) fi for version in ${versions[*]} @@ -23,6 +23,7 @@ do echo "Stopping uptime kuma..." docker stop uptimekuma > /dev/null + sleep 1 echo '' done diff --git a/scripts/build_inputs.py b/scripts/build_inputs.py index d8a9e18..9d8c8a2 100644 --- a/scripts/build_inputs.py +++ b/scripts/build_inputs.py @@ -1,37 +1,57 @@ import glob import re +from bs4 import BeautifulSoup -from utils import deduplicate_list, parse_vue_template +from utils import deduplicate_list, parse_vue_template, diff, type_html_to_py -ROOT = "uptime-kuma" +input_ignores = { + "settings": [ + "$root.styleElapsedTime" + ] +} def parse_model_keys(content, object_name): - match = re.findall(object_name + r"\.[0-9a-zA-Z_$]+", content) - keys = [] - for m in match: - key = m.replace(object_name + ".", "") - keys.append(key) - keys = deduplicate_list(keys) - return keys + soup = BeautifulSoup(content, "html.parser") + + soup_inputs = soup.find_all(attrs={"v-model": True}) + inputs = [] + for soup_input in soup_inputs: + key = soup_input["v-model"] + if key in input_ignores.get(object_name, []): + continue + else: + key = re.sub(r'^' + object_name + r'\.', "", key) + type_ = soup_input.get("type", "text") + type_ = type_html_to_py(type_) + if type_ == "bool": + value = True if soup_input.get("checked") else False + else: + value = soup_input.get("value") + inputs.append({ + "key": key, + "type": type_, + "default": value, + }) + return inputs -def parse_proxy_keys(): - content = parse_vue_template(f"{ROOT}/src/components/ProxyDialog.vue") +def parse_proxy_keys(root): + content = parse_vue_template(f"{root}/src/components/ProxyDialog.vue") keys = parse_model_keys(content, "proxy") return keys -def parse_notification_keys(): - content = parse_vue_template(f"{ROOT}/src/components/NotificationDialog.vue") +def parse_notification_keys(root): + content = parse_vue_template(f"{root}/src/components/NotificationDialog.vue") keys = parse_model_keys(content, "notification") return keys -def parse_settings_keys(): +def parse_settings_keys(root): all_keys = [] - for path in glob.glob('uptime-kuma/src/components/settings/*'): + for path in glob.glob(f'{root}/src/components/settings/*'): content = parse_vue_template(path) keys = parse_model_keys(content, "settings") all_keys.extend(keys) @@ -39,20 +59,20 @@ def parse_settings_keys(): return all_keys -def parse_monitor_keys(): - content = parse_vue_template(f"{ROOT}/src/pages/EditMonitor.vue") +def parse_monitor_keys(root): + content = parse_vue_template(f"{root}/src/pages/EditMonitor.vue") keys = parse_model_keys(content, "monitor") return keys -def parse_status_page_keys(): +def parse_status_page_keys(root): all_keys = ["id"] - content = parse_vue_template(f"{ROOT}/src/pages/StatusPage.vue") + content = parse_vue_template(f"{root}/src/pages/StatusPage.vue") keys = parse_model_keys(content, "config") all_keys.extend(keys) - content = parse_vue_template(f"{ROOT}/src/pages/ManageStatusPage.vue") + content = parse_vue_template(f"{root}/src/pages/ManageStatusPage.vue") keys = parse_model_keys(content, "statusPage") all_keys.extend(keys) @@ -60,30 +80,28 @@ def parse_status_page_keys(): return all_keys -def parse_maintenance_keys(): - content = parse_vue_template(f"{ROOT}/src/pages/EditMaintenance.vue") +def parse_maintenance_keys(root): + content = parse_vue_template(f"{root}/src/pages/EditMaintenance.vue") keys = parse_model_keys(content, "maintenance") return keys def main(): - proxy_keys = parse_proxy_keys() - print("proxy:", proxy_keys) + root_old = "uptime-kuma-old" + root_new = "uptime-kuma" - notification_keys = parse_notification_keys() - print("notification:", notification_keys) - - settings_keys = parse_settings_keys() - print("settings:", settings_keys) - - monitor_keys = parse_monitor_keys() - print("monitor:", monitor_keys) - - status_page_keys = parse_status_page_keys() - print("status_page:", status_page_keys) - - maintenance_keys = parse_maintenance_keys() - print("maintenance:", maintenance_keys) + for name, func in [ + ["proxy", parse_proxy_keys], + ["notification", parse_notification_keys], + ["settings", parse_settings_keys], + ["monitor", parse_monitor_keys], + ["status_page", parse_status_page_keys], + ["maintenance", parse_maintenance_keys], + ]: + keys_old = func(root_old) + keys_new = func(root_new) + print(f"{name}:") + diff(keys_old, keys_new) if __name__ == "__main__": diff --git a/scripts/build_models.py b/scripts/build_models.py index b5f831f..57beb19 100644 --- a/scripts/build_models.py +++ b/scripts/build_models.py @@ -1,10 +1,6 @@ import re -from pprint import pprint -from utils import deduplicate_list - - -ROOT = "uptime-kuma" +from utils import deduplicate_list, diff def parse_json_keys(data): @@ -22,17 +18,8 @@ def parse_json_keys(data): return keys -# def parse_object_keys(code, object_name): -# match = re.findall(object_name + r'\.[0-9a-zA-Z_$]+', code) -# keys = [] -# for m in match: -# key = m.replace(object_name + ".", "") -# keys.append(key) -# return list(set(keys)) - - -def parse_heartbeat(): - with open(f'{ROOT}/server/model/heartbeat.js') as f: +def parse_heartbeat(root): + with open(f'{root}/server/model/heartbeat.js') as f: content = f.read() all_keys = [] match = re.search(r'toJSON\(\) {\s+return.*{([^}]+)}', content) @@ -47,8 +34,8 @@ def parse_heartbeat(): return all_keys -def parse_incident(): - with open(f'{ROOT}/server/model/incident.js') as f: +def parse_incident(root): + with open(f'{root}/server/model/incident.js') as f: content = f.read() match = re.search(r'toPublicJSON\(\) {\s+return.*{([^}]+)}', content) data = match.group(1) @@ -56,9 +43,9 @@ def parse_incident(): return keys -def parse_monitor(): +def parse_monitor(root): # todo: toPublicJSON ??? - with open(f'{ROOT}/server/model/monitor.js') as f: + with open(f'{root}/server/model/monitor.js') as f: content = f.read() matches = re.findall(r'data = {([^}]+)}', content) all_keys = [] @@ -70,8 +57,8 @@ def parse_monitor(): return all_keys -def parse_proxy(): - with open(f'{ROOT}/server/model/proxy.js') as f: +def parse_proxy(root): + with open(f'{root}/server/model/proxy.js') as f: content = f.read() match = re.search(r'toJSON\(\) {\s+return.*{([^}]+)}', content) data = match.group(1) @@ -102,7 +89,7 @@ def parse_proxy(): # # input (add, edit proxy) # def parse_proxy2(): -# with open(f'{ROOT}/server/proxy.js') as f: +# with open(f'{root}/server/proxy.js') as f: # content = f.read() # # code = parse_function(r'async save\([^)]+\) ', content) @@ -110,8 +97,8 @@ def parse_proxy(): # return keys -def parse_status_page(): - with open(f'{ROOT}/server/model/status_page.js') as f: +def parse_status_page(root): + with open(f'{root}/server/model/status_page.js') as f: content = f.read() all_keys = [] match = re.search(r'toJSON\(\) {\s+return.*{([^}]+)}', content) @@ -126,8 +113,8 @@ def parse_status_page(): return all_keys -def parse_tag(): - with open(f'{ROOT}/server/model/tag.js') as f: +def parse_tag(root): + with open(f'{root}/server/model/tag.js') as f: content = f.read() match = re.search(r'toJSON\(\) {\s+return.*{([^}]+)}', content) data = match.group(1) @@ -135,33 +122,22 @@ def parse_tag(): return keys -print("heartbeat") -pprint(parse_heartbeat()) -print("") +if __name__ == "__main__": + root_old = "uptime-kuma-old" + root_new = "uptime-kuma" -print("incident") -pprint(parse_incident()) -print("") - -print("monitor") -pprint(parse_monitor()) -print("") - -print("proxy") -pprint(parse_proxy()) -print("") - -# print("prox2") -# pprint(parse_proxy2()) -# print("") - -print("status page") -pprint(parse_status_page()) -print("") - -print("tag") -pprint(parse_tag()) -print("") + for name, func in [ + ["heartbeat", parse_heartbeat], + ["incident", parse_incident], + ["monitor", parse_monitor], + ["proxy", parse_proxy], + ["status page", parse_status_page], + ["tag", parse_tag], + ]: + keys_old = func(root_old) + keys_new = func(root_new) + print(f"{name}:") + diff(keys_old, keys_new) # TODO: diff --git a/scripts/build_monitor_types.py b/scripts/build_monitor_types.py index 242136c..90a5942 100644 --- a/scripts/build_monitor_types.py +++ b/scripts/build_monitor_types.py @@ -3,6 +3,32 @@ from bs4 import BeautifulSoup from utils import parse_vue_template, write_to_file +titles = { + "http": "HTTP(s)", + "port": "TCP Port", + "ping": "Ping", + "keyword": "HTTP(s) - Keyword", + "grpc-keyword": "gRPC(s) - Keyword", + "dns": "DNS", + "docker": "Docker Container", + "push": "Push", + "steam": "Steam Game Server", + "gamedig": "GameDig", + "mqtt": "MQTT", + "sqlserver": "Microsoft SQL Server", + "postgres": "PostgreSQL", + "mysql": "MySQL/MariaDB", + "mongodb": "MongoDB", + "radius": "Radius", + "redis": "Redis", + "group": "Group", + "json-query": "HTTP(s) - Json Query", + "real-browser": "HTTP(s) - Browser Engine (Chrome/Chromium)", + "kafka-producer": "Kafka Producer", + "tailscale-ping": "Tailscale Ping" +} + + def parse_monitor_types(): content = parse_vue_template("uptime-kuma/src/pages/EditMonitor.vue") @@ -10,10 +36,13 @@ def parse_monitor_types(): select = soup.find("select", id="type") options = select.find_all("option") - types = [] + types = {} for o in options: type_ = o.attrs["value"] - types.append(type_) + types[type_] = { + "value": type_, + "title": titles[type_] + } return types diff --git a/scripts/build_notifications.py b/scripts/build_notifications.py index 38e796a..1357508 100644 --- a/scripts/build_notifications.py +++ b/scripts/build_notifications.py @@ -3,7 +3,7 @@ import re from bs4 import BeautifulSoup -from utils import deduplicate_list, write_to_file +from utils import deduplicate_list, write_to_file, type_html_to_py # deprecated or wrong inputs @@ -26,6 +26,10 @@ ignored_inputs = { ] } +input_overwrites = { + "showAdditionalHeadersField": "webhookAdditionalHeaders" +} + titles = { "alerta": "Alerta", "AlertNow": "AlertNow", @@ -48,6 +52,7 @@ titles = { "OneBot": "OneBot", "Opsgenie": "Opsgenie", "PagerDuty": "PagerDuty", + "PagerTree": "PagerTree", "pushbullet": "Pushbullet", "PushByTechulus": "Push by Techulus", "pushover": "Pushover", @@ -76,10 +81,14 @@ titles = { "SMSManager": "SmsManager (smsmanager.cz)", "WeCom": "WeCom", "ServerChan": "ServerChan", + "nostr": "Nostr", + "FlashDuty": "FlashDuty", + "smsc": "SMSC", } -def build_notification_providers(root): +def build_notification_providers(): + root = "uptime-kuma" providers = {} # get providers and input names @@ -96,7 +105,7 @@ def build_notification_providers(root): inputs = [i.strip() for i in inputs] providers[name] = { - "title": titles.get(name, name), + "title": titles[name], "inputs": {}, } for input_ in inputs: @@ -117,15 +126,15 @@ def build_notification_providers(root): conditions = {} attrs = input_.attrs v_model = attrs.get("v-model") - param_name = re.match(r'\$parent.notification.(.*)$', v_model).group(1) + + v_model_overwrite = input_overwrites.get(v_model) + if v_model_overwrite: + param_name = v_model_overwrite + else: + param_name = re.match(r'\$parent.notification.(.*)$', v_model).group(1) type_ = attrs.get("type") - if type_ == "number": - type_ = "int" - elif type_ == "checkbox": - type_ = "bool" - else: - type_ = "str" + type_ = type_html_to_py(type_) required_true_values = ['', 'true'] if attrs.get("required") in required_true_values or attrs.get(":required") in required_true_values: @@ -157,17 +166,7 @@ def build_notification_providers(root): return providers -def diff(old, new): - for i in new: - if i not in old: - print("+", i) - for i in old: - if i not in new: - print("-", i) - print("") - - -notification_providers = build_notification_providers("uptime-kuma") +notification_providers = build_notification_providers() notification_provider_conditions = {} for notification_provider in notification_providers: @@ -181,11 +180,3 @@ write_to_file( notification_providers=notification_providers, notification_provider_conditions=notification_provider_conditions ) - -# notification_providers_old = build_notification_providers("uptime-kuma-old") -# notification_providers_new = build_notification_providers("uptime-kuma") -# diff(notification_providers_old, notification_providers_new) -# -# notification_provider_conditions_old = build_notification_provider_conditions("uptime-kuma-old") -# notification_provider_conditions_new = build_notification_provider_conditions("uptime-kuma") -# diff(notification_provider_conditions_old, notification_provider_conditions_new) diff --git a/scripts/models/notification.py b/scripts/models/notification.py deleted file mode 100644 index 44822e9..0000000 --- a/scripts/models/notification.py +++ /dev/null @@ -1,6 +0,0 @@ -notification = [ - "type", - "isDefault", - "userId", - "applyExisting", -] diff --git a/scripts/monitor_type.py.j2 b/scripts/monitor_type.py.j2 index 61b69d5..5a76bb0 100644 --- a/scripts/monitor_type.py.j2 +++ b/scripts/monitor_type.py.j2 @@ -2,7 +2,10 @@ from enum import Enum class MonitorType(str, Enum): -{%- for name in monitor_types %} - {{ name.upper() }} = "{{ name }}" -{%- endfor %} + """Enumerate monitor types.""" + {{""}} +{%- for type_ in monitor_types.values() %} + {{ type_["value"].upper().replace("-", "_") }} = "{{ type_["value"] }}" + """{{ type_["title"] }}""" +{% endfor -%} diff --git a/scripts/utils.py b/scripts/utils.py index fcf52d1..d507a02 100644 --- a/scripts/utils.py +++ b/scripts/utils.py @@ -24,3 +24,23 @@ def write_to_file(template, destination, **kwargs): rendered = template.render(**kwargs) with open(destination, "w") as f: f.write(rendered) + + +def diff(old, new): + for i in new: + if i not in old: + print("+", i) + for i in old: + if i not in new: + print("-", i) + print("") + + +def type_html_to_py(type_): + if type_ == "number": + type_ = "int" + elif type_ == "checkbox": + type_ = "bool" + else: + type_ = "str" + return type_ diff --git a/tests/test_docker_host.py b/tests/test_docker_host.py index 4b82757..dbd86c2 100644 --- a/tests/test_docker_host.py +++ b/tests/test_docker_host.py @@ -1,4 +1,5 @@ import unittest +from packaging.version import parse as parse_version from uptime_kuma_api import DockerType, UptimeKumaException from uptime_kuma_test_case import UptimeKumaTestCase @@ -16,8 +17,10 @@ class TestDockerHost(UptimeKumaTestCase): } # test docker host - with self.assertRaisesRegex(UptimeKumaException, r'connect ENOENT /var/run/docker.sock'): - self.api.test_docker_host(**expected_docker_host) + if parse_version(self.api.version) != parse_version("1.23.0"): + # test_docker_host does not work in 1.23.0 (https://github.com/louislam/uptime-kuma/issues/3605) + with self.assertRaisesRegex(UptimeKumaException, r'connect ENOENT /var/run/docker.sock'): + self.api.test_docker_host(**expected_docker_host) # add docker host r = self.api.add_docker_host(**expected_docker_host) diff --git a/tests/test_monitor.py b/tests/test_monitor.py index 4a26481..634f280 100644 --- a/tests/test_monitor.py +++ b/tests/test_monitor.py @@ -324,6 +324,53 @@ class TestMonitor(UptimeKumaTestCase): } self.do_test_monitor_type(expected_monitor) + def test_monitor_type_json_query(self): + if parse_version(self.api.version) < parse_version("1.23"): + self.skipTest("Unsupported in this Uptime Kuma version") + + expected_monitor = { + "type": MonitorType.JSON_QUERY, + "name": "monitor 1", + "url": "http://127.0.0.1", + "jsonPath": "address.country", + "expectedValue": "germany", + } + self.do_test_monitor_type(expected_monitor) + + def test_monitor_type_real_browser(self): + if parse_version(self.api.version) < parse_version("1.23"): + self.skipTest("Unsupported in this Uptime Kuma version") + + expected_monitor = { + "type": MonitorType.REAL_BROWSER, + "name": "monitor 1", + "url": "http://127.0.0.1", + } + self.do_test_monitor_type(expected_monitor) + + def test_monitor_type_kafka_producer(self): + if parse_version(self.api.version) < parse_version("1.23"): + self.skipTest("Unsupported in this Uptime Kuma version") + + expected_monitor = { + "type": MonitorType.KAFKA_PRODUCER, + "name": "monitor 1", + "kafkaProducerTopic": "topic", + "kafkaProducerMessage": "message", + } + self.do_test_monitor_type(expected_monitor) + + def test_monitor_type_tailscale_ping(self): + if parse_version(self.api.version) < parse_version("1.23"): + self.skipTest("Unsupported in this Uptime Kuma version") + + expected_monitor = { + "type": MonitorType.TAILSCALE_PING, + "name": "monitor 1", + "hostname": "127.0.0.1" + } + self.do_test_monitor_type(expected_monitor) + def test_delete_not_existing_monitor(self): with self.assertRaises(UptimeKumaException): self.api.delete_monitor(42) diff --git a/uptime_kuma_api/api.py b/uptime_kuma_api/api.py index 6070e93..1721bde 100644 --- a/uptime_kuma_api/api.py +++ b/uptime_kuma_api/api.py @@ -278,7 +278,11 @@ def _check_arguments_monitor(kwargs) -> None: MonitorType.MONGODB: [], MonitorType.RADIUS: ["radiusUsername", "radiusPassword", "radiusSecret", "radiusCalledStationId", "radiusCallingStationId"], MonitorType.REDIS: [], - MonitorType.GROUP: [] + MonitorType.GROUP: [], + MonitorType.JSON_QUERY: ["url", "jsonPath", "expectedValue"], + MonitorType.REAL_BROWSER: ["url"], + MonitorType.KAFKA_PRODUCER: ["kafkaProducerTopic", "kafkaProducerMessage"], + MonitorType.TAILSCALE_PING: ["hostname"], } type_ = kwargs["type"] required_args = required_args_by_type[type_] @@ -304,8 +308,45 @@ def _check_arguments_monitor(kwargs) -> None: ) _check_argument_conditions(conditions, kwargs) - if kwargs["accepted_statuscodes"] and not all([type(i) == str for i in kwargs["accepted_statuscodes"]]): - raise TypeError("Accepted status codes are not all strings") + allowed_accepted_statuscodes = [ + "100-199", + "200-299", + "300-399", + "400-499", + "500-599", + ] + [ + str(i) for i in range(100, 999 + 1) + ] + accepted_statuscodes = kwargs["accepted_statuscodes"] + for accepted_statuscode in accepted_statuscodes: + if accepted_statuscode not in allowed_accepted_statuscodes: + raise ValueError(f"Unknown accepted_statuscodes value: {allowed_accepted_statuscodes}") + + dns_resolve_type = kwargs["dns_resolve_type"] + if dns_resolve_type not in [ + "A", + "AAAA", + "CAA", + "CNAME", + "MX", + "NS", + "PTR", + "SOA", + "SRV", + "TXT", + ]: + raise ValueError(f"Unknown dns_resolve_type value: {dns_resolve_type}") + + if type_ == MonitorType.KAFKA_PRODUCER: + kafkaProducerSaslOptions_mechanism = kwargs["kafkaProducerSaslOptions"]["mechanism"] + if kafkaProducerSaslOptions_mechanism not in [ + "None", + "plain", + "scram-sha-256", + "scram-sha-512", + "aws", + ]: + raise ValueError(f"Unknown kafkaProducerSaslOptions[\"mechanism\"] value: {kafkaProducerSaslOptions_mechanism}") def _check_arguments_notification(kwargs) -> None: @@ -653,12 +694,16 @@ class UptimeKumaApi(object): notificationIDList: list = None, httpBodyEncoding: str = "json", - # HTTP, KEYWORD + # HTTP, KEYWORD, JSON_QUERY, REAL_BROWSER url: str = None, + + # HTTP, KEYWORD, GRPC_KEYWORD + maxredirects: int = 10, + accepted_statuscodes: list[str] = None, + + # HTTP, KEYWORD, JSON_QUERY expiryNotification: bool = False, ignoreTls: bool = False, - maxredirects: int = 10, - accepted_statuscodes: list = None, proxyId: int = None, method: str = "GET", body: str = None, @@ -671,9 +716,16 @@ class UptimeKumaApi(object): basic_auth_pass: str = None, authDomain: str = None, authWorkstation: str = None, + oauth_auth_method: str = "client_secret_basic", + oauth_token_url: str = None, + oauth_client_id: str = None, + oauth_client_secret: str = None, + oauth_scopes: str = None, + timeout: int = 48, # KEYWORD keyword: str = None, + invertKeyword: bool = False, # GRPC_KEYWORD grpcUrl: str = None, @@ -684,13 +736,13 @@ class UptimeKumaApi(object): grpcBody: str = None, grpcMetadata: str = None, - # DNS, PING, STEAM, MQTT + # PORT, PING, DNS, STEAM, MQTT, RADIUS, TAILSCALE_PING hostname: str = None, # PING packetSize: int = 56, - # DNS, STEAM, MQTT, RADIUS + # PORT, DNS, STEAM, MQTT, RADIUS port: int = None, # DNS @@ -698,10 +750,10 @@ class UptimeKumaApi(object): dns_resolve_type: str = "A", # MQTT - mqttUsername: str = None, - mqttPassword: str = None, - mqttTopic: str = None, - mqttSuccessMessage: str = None, + mqttUsername: str = "", + mqttPassword: str = "", + mqttTopic: str = "", + mqttSuccessMessage: str = "", # SQLSERVER, POSTGRES, MYSQL, MONGODB, REDIS databaseConnectionString: str = None, @@ -721,8 +773,27 @@ class UptimeKumaApi(object): radiusCallingStationId: str = None, # GAMEDIG - game: str = None + game: str = None, + gamedigGivenPortOnly: bool = True, + + # JSON_QUERY + jsonPath: str = None, + expectedValue: str = None, + + # KAFKA_PRODUCER + kafkaProducerBrokers: list[str] = None, + kafkaProducerTopic: str = None, + kafkaProducerMessage: str = None, + kafkaProducerSsl: bool = False, + kafkaProducerAllowAutoTopicCreation: bool = False, + kafkaProducerSaslOptions: dict = None, ) -> dict: + if accepted_statuscodes is None: + accepted_statuscodes = ["200-299"] + + if notificationIDList is None: + notificationIDList = {} + data = { "type": type, "name": name, @@ -733,26 +804,37 @@ class UptimeKumaApi(object): "upsideDown": upsideDown, "resendInterval": resendInterval, "description": description, - "httpBodyEncoding": httpBodyEncoding + "httpBodyEncoding": httpBodyEncoding, } if parse_version(self.version) >= parse_version("1.22"): data.update({ - "parent": parent + "parent": parent, }) if type in [MonitorType.KEYWORD, MonitorType.GRPC_KEYWORD]: data.update({ "keyword": keyword, }) + if parse_version(self.version) >= parse_version("1.23"): + data.update({ + "invertKeyword": invertKeyword, + }) - # HTTP, KEYWORD + # HTTP, KEYWORD, JSON_QUERY, REAL_BROWSER data.update({ "url": url, - "expiryNotification": expiryNotification, - "ignoreTls": ignoreTls, + }) + + # HTTP, KEYWORD, GRPC_KEYWORD + data.update({ "maxredirects": maxredirects, "accepted_statuscodes": accepted_statuscodes, + }) + + data.update({ + "expiryNotification": expiryNotification, + "ignoreTls": ignoreTls, "proxyId": proxyId, "method": method, "body": body, @@ -760,6 +842,11 @@ class UptimeKumaApi(object): "authMethod": authMethod, }) + if parse_version(self.version) >= parse_version("1.23"): + data.update({ + "timeout": timeout, + }) + if authMethod in [AuthMethod.HTTP_BASIC, AuthMethod.NTLM]: data.update({ "basic_auth_user": basic_auth_user, @@ -779,6 +866,15 @@ class UptimeKumaApi(object): "tlsCa": tlsCa, }) + if authMethod == AuthMethod.OAUTH2_CC: + data.update({ + "oauth_auth_method": oauth_auth_method, + "oauth_token_url": oauth_token_url, + "oauth_client_id": oauth_client_id, + "oauth_client_secret": oauth_client_secret, + "oauth_scopes": oauth_scopes, + }) + # GRPC_KEYWORD if type == MonitorType.GRPC_KEYWORD: data.update({ @@ -791,7 +887,7 @@ class UptimeKumaApi(object): "grpcMetadata": grpcMetadata, }) - # PORT, PING, DNS, STEAM, MQTT + # PORT, PING, DNS, STEAM, MQTT, RADIUS, TAILSCALE_PING data.update({ "hostname": hostname, }) @@ -840,7 +936,7 @@ class UptimeKumaApi(object): if type == MonitorType.DOCKER: data.update({ "docker_container": docker_container, - "docker_host": docker_host + "docker_host": docker_host, }) # RADIUS @@ -850,15 +946,42 @@ class UptimeKumaApi(object): "radiusPassword": radiusPassword, "radiusSecret": radiusSecret, "radiusCalledStationId": radiusCalledStationId, - "radiusCallingStationId": radiusCallingStationId + "radiusCallingStationId": radiusCallingStationId, }) # GAMEDIG if type == MonitorType.GAMEDIG: data.update({ - "game": game + "game": game, + }) + if parse_version(self.version) >= parse_version("1.23"): + data.update({ + "gamedigGivenPortOnly": gamedigGivenPortOnly, + }) + + # JSON_QUERY + if type == MonitorType.JSON_QUERY: + data.update({ + "jsonPath": jsonPath, + "expectedValue": expectedValue, }) + # KAFKA_PRODUCER + if type == MonitorType.KAFKA_PRODUCER: + if kafkaProducerBrokers is None: + kafkaProducerBrokers = [] + if not kafkaProducerSaslOptions: + kafkaProducerSaslOptions = { + "mechanism": "None", + } + data.update({ + "kafkaProducerBrokers": kafkaProducerBrokers, + "kafkaProducerTopic": kafkaProducerTopic, + "kafkaProducerMessage": kafkaProducerMessage, + "kafkaProducerSsl": kafkaProducerSsl, + "kafkaProducerAllowAutoTopicCreation": kafkaProducerAllowAutoTopicCreation, + "kafkaProducerSaslOptions": kafkaProducerSaslOptions, + }) return data def _build_maintenance_data( @@ -926,6 +1049,7 @@ class UptimeKumaApi(object): customCSS: str = "", footerText: str = None, showPoweredBy: bool = True, + showCertificateExpiry: bool = False, icon: str = "/icon.svg", publicGroupList: list = None @@ -954,8 +1078,12 @@ class UptimeKumaApi(object): "googleAnalyticsId": googleAnalyticsId, "customCSS": customCSS, "footerText": footerText, - "showPoweredBy": showPoweredBy + "showPoweredBy": showPoweredBy, } + if parse_version(self.version) >= parse_version("1.23"): + config.update({ + "showCertificateExpiry": showCertificateExpiry, + }) return slug, config, icon, publicGroupList # monitor @@ -1082,9 +1210,11 @@ class UptimeKumaApi(object): 'dns_resolve_type': 'A', 'docker_container': None, 'docker_host': None, + 'expectedValue': None, 'expiryNotification': False, 'forceInactive': False, 'game': None, + 'gamedigGivenPortOnly': True, 'grpcBody': None, 'grpcEnableTls': False, 'grpcMetadata': None, @@ -1099,17 +1229,30 @@ class UptimeKumaApi(object): 'ignoreTls': False, 'includeSensitiveData': True, 'interval': 60, + 'invertKeyword': False, + 'jsonPath': None, + 'kafkaProducerAllowAutoTopicCreation': False, + 'kafkaProducerBrokers': None, + 'kafkaProducerMessage': None, + 'kafkaProducerSaslOptions': None, + 'kafkaProducerSsl': False, + 'kafkaProducerTopic': None, 'keyword': None, 'maintenance': False, 'maxredirects': 10, 'maxretries': 0, 'method': 'GET', - 'mqttPassword': None, - 'mqttSuccessMessage': None, - 'mqttTopic': None, - 'mqttUsername': None, + 'mqttPassword': '', + 'mqttSuccessMessage': '', + 'mqttTopic': '', + 'mqttUsername': '', 'name': 'monitor 1', 'notificationIDList': [1, 2], + 'oauth_auth_method': None, + 'oauth_client_id': None, + 'oauth_client_secret': None, + 'oauth_scopes': None, + 'oauth_token_url': None, 'packetSize': 56, 'parent': None, 'pathName': 'monitor 1', @@ -1123,7 +1266,9 @@ class UptimeKumaApi(object): 'radiusUsername': None, 'resendInterval': 0, 'retryInterval': 60, + 'screenshot': None, 'tags': [], + 'timeout': 48, 'tlsCa': None, 'tlsCert': None, 'tlsKey': None, @@ -1279,6 +1424,23 @@ class UptimeKumaApi(object): r = self._call('getGameList') return r.get("gameList") + def test_chrome(self, executable) -> dict: + """ + Test if the chrome executable is valid and return the version. + + :return: The server response. + :rtype: dict + :raises UptimeKumaException: If the server returns an error. + + Example:: + + >>> api.test_chrome("/usr/bin/chromium") + { + 'msg': 'Found Chromium/Chrome. Version: 90.0.4430.212' + } + """ + return self._call('testChrome', executable) + @append_docstring(monitor_docstring("add")) def add_monitor(self, **kwargs) -> dict: """ @@ -1799,8 +1961,8 @@ class UptimeKumaApi(object): 'description': 'description 1', 'domainNameList': [], 'footerText': None, - 'icon': '/icon.svg', 'googleAnalyticsId': '', + 'icon': '/icon.svg', 'id': 1, 'incident': { 'content': 'content 1', @@ -1819,7 +1981,8 @@ class UptimeKumaApi(object): { 'id': 1, 'name': 'monitor 1', - 'sendUrl': False + 'sendUrl': False, + 'type': 'http' } ], 'name': 'Services', @@ -1827,6 +1990,7 @@ class UptimeKumaApi(object): } ], 'published': True, + 'showCertificateExpiry': False, 'showPoweredBy': False, 'showTags': False, 'slug': 'slug1', @@ -1919,6 +2083,7 @@ class UptimeKumaApi(object): :param str, optional customCSS: Custom CSS, defaults to "" :param str, optional footerText: Custom Footer, defaults to None :param bool, optional showPoweredBy: Show Powered By, defaults to True + :param bool, optional showCertificateExpiry: Show Certificate Expiry, defaults to False :param str, optional icon: Icon, defaults to "/icon.svg" :param list, optional publicGroupList: Public Group List, defaults to None :return: The server response. @@ -2327,11 +2492,12 @@ class UptimeKumaApi(object): >>> api.info() { - 'version': '1.19.2', - 'latestVersion': '1.19.2', - 'primaryBaseURL': None, + 'isContainer': True, + 'latestVersion': '1.23.1', + 'primaryBaseURL': '', 'serverTimezone': 'Europe/Berlin', - 'serverTimezoneOffset': '+01:00' + 'serverTimezoneOffset': '+02:00', + 'version': '1.23.1' } """ r = self._get_event_data(Event.INFO) @@ -2525,10 +2691,12 @@ class UptimeKumaApi(object): { 'checkBeta': False, 'checkUpdate': False, + 'chromeExecutable': '', 'disableAuth': False, 'dnsCache': True, 'entryPage': 'dashboard', 'keepDataPeriodDays': 180, + 'nscd': False, 'primaryBaseURL': '', 'searchEngineIndex': False, 'serverTimezone': 'Europe/Berlin', @@ -2561,7 +2729,9 @@ class UptimeKumaApi(object): searchEngineIndex: bool = False, primaryBaseURL: str = "", steamAPIKey: str = "", + nscd: bool = False, dnsCache: bool = False, + chromeExecutable: str = "", # notifications tlsExpiryNotifyDays: list = None, @@ -2584,7 +2754,9 @@ class UptimeKumaApi(object): :param bool, optional searchEngineIndex: Search Engine Visibility, defaults to False :param str, optional primaryBaseURL: Primary Base URL, defaults to "" :param str, optional steamAPIKey: Steam API Key. For monitoring a Steam Game Server you need a Steam Web-API key., defaults to "" + :param bool, optional nscd: Enable NSCD (Name Service Cache Daemon) for caching all DNS requests, defaults to False :param bool, optional dnsCache: True to enable DNS Cache. It may be not working in some IPv6 environments, disable it if you encounter any issues., defaults to False + :param str, optional chromeExecutable: Chrome/Chromium Executable, defaults to "" :param list, optional tlsExpiryNotifyDays: TLS Certificate Expiry. HTTPS Monitors trigger notification when TLS certificate expires in., defaults to None :param bool, optional disableAuth: Disable Authentication, defaults to False :param bool, optional trustProxy: Trust Proxy. Trust 'X-Forwarded-\*' headers. If you want to get the correct client IP and your Uptime Kuma is behind such as Nginx or Apache, you should enable this., defaults to False @@ -2634,6 +2806,16 @@ class UptimeKumaApi(object): "disableAuth": disableAuth, "trustProxy": trustProxy } + + if parse_version(self.version) >= parse_version("1.23"): + data.update({ + "chromeExecutable": chromeExecutable, + }) + if parse_version(self.version) >= parse_version("1.23.1"): + data.update({ + "nscd": nscd, + }) + return self._call('setSettings', (data, password)) def change_password(self, old_password: str, new_password: str) -> dict: @@ -2688,7 +2870,7 @@ class UptimeKumaApi(object): } """ if import_handle not in ["overwrite", "skip", "keep"]: - raise ValueError() + raise ValueError(f"Unknown import_handle value: {import_handle}") return self._call('uploadBackup', (json_data, import_handle)) # 2FA diff --git a/uptime_kuma_api/auth_method.py b/uptime_kuma_api/auth_method.py index ff36b07..d50267c 100644 --- a/uptime_kuma_api/auth_method.py +++ b/uptime_kuma_api/auth_method.py @@ -15,3 +15,6 @@ class AuthMethod(str, Enum): MTLS = "mtls" """mTLS Authentication.""" + + OAUTH2_CC = "oauth2-cc" + """OAuth2: Client Credentials""" diff --git a/uptime_kuma_api/docstrings.py b/uptime_kuma_api/docstrings.py index 0d1e2fe..c42c3d2 100644 --- a/uptime_kuma_api/docstrings.py +++ b/uptime_kuma_api/docstrings.py @@ -44,12 +44,30 @@ def monitor_docstring(mode) -> str: :param str, optional basic_auth_pass: Password for ``authMethod`` :attr:`~.AuthMethod.HTTP_BASIC` and :attr:`~.AuthMethod.NTLM`, defaults to None :param str, optional authDomain: Domain for ``authMethod`` :attr:`~.AuthMethod.NTLM`, defaults to None :param str, optional authWorkstation: Workstation for ``authMethod`` :attr:`~.AuthMethod.NTLM`, defaults to None + :param str, optional oauth_auth_method: Authentication Method, defaults to None + :param str, optional oauth_token_url: OAuth Token URL, defaults to None + :param str, optional oauth_client_id: Client ID, defaults to None + :param str, optional oauth_client_secret: Client Secret, defaults to None + :param str, optional oauth_scopes: OAuth Scope, defaults to None + :param int, optional timeout: Request Timeout, defaults to None :param str, optional keyword: Keyword. Search keyword in plain HTML or JSON response. The search is case-sensitive., defaults to None + :param bool, optional invertKeyword: Invert Keyword. Look for the keyword to be absent rather than present., defaults to False :param str, optional hostname: Hostname, defaults to None :param int, optional packetSize: Packet Size, defaults to None :param int, optional port: Port, ``type`` :attr:`~.MonitorType.DNS` defaults to ``53`` and ``type`` :attr:`~.MonitorType.RADIUS` defaults to ``1812`` :param str, optional dns_resolve_server: Resolver Server, defaults to "1.1.1.1" - :param str, optional dns_resolve_type: Resource Record Type, defaults to "A" + :param str, optional dns_resolve_type: Resource Record Type, defaults to "A". Available values are: + + - "A" + - "AAAA" + - "CAA" + - "CNAME" + - "MX" + - "NS" + - "PTR" + - "SOA" + - "SRV" + - "TXT" :param str, optional mqttUsername: MQTT Username, defaults to None :param str, optional mqttPassword: MQTT Password, defaults to None :param str, optional mqttTopic: MQTT Topic, defaults to None @@ -64,6 +82,29 @@ def monitor_docstring(mode) -> str: :param str, optional radiusCalledStationId: Called Station Id. Identifier of the called device., defaults to None :param str, optional radiusCallingStationId: Calling Station Id. Identifier of the calling device., defaults to None :param str, optional game: Game, defaults to None + :param bool, optional gamedigGivenPortOnly: Gamedig: Guess Port. The port used by Valve Server Query Protocol may be different from the client port. Try this if the monitor cannot connect to your server., defaults to False + :param str, optional jsonPath: Json Query, defaults to None + :param str, optional expectedValue: Expected Value, defaults to None + :param str, optional kafkaProducerBrokers: Kafka Broker list, defaults to None + :param str, optional kafkaProducerTopic: Kafka Topic Name, defaults to None + :param str, optional kafkaProducerMessage: Kafka Producer Message, defaults to None + :param bool, optional kafkaProducerSsl: Enable Kafka SSL, defaults to False + :param bool, optional kafkaProducerAllowAutoTopicCreation: Enable Kafka Producer Auto Topic Creation, defaults to False + :param dict, optional kafkaProducerSaslOptions: Kafka SASL Options + + - **mechanism** (*str*, *optional*): Mechanism, defaults to "None". Available values are: + + - "None" + - "plain" + - "scram-sha-256" + - "scram-sha-512" + - "aws" + - **username** (*str*, *optional*): Username, defaults to None + - **password** (*str*, *optional*): Password, defaults to None + - **authorizationIdentity** (*str*, *optional*): Authorization Identity, defaults to None + - **accessKeyId** (*str*, *optional*): AccessKey Id, defaults to None + - **secretAccessKey** (*str*, *optional*): Secret AccessKey, defaults to None + - **sessionToken** (*str*, *optional*): Session Token, defaults to None """ @@ -100,6 +141,8 @@ def notification_docstring(mode) -> str: :param str, optional discordWebhookUrl: Notification option for ``type`` :attr:`~.NotificationType.DISCORD`. :param str discordPrefixMessage: Notification option for ``type`` :attr:`~.NotificationType.DISCORD`. :param str, optional feishuWebHookUrl: Notification option for ``type`` :attr:`~.NotificationType.FEISHU`. + :param str, optional flashdutySeverity: Notification option for ``type`` :attr:`~.NotificationType.FLASHDUTY`. + :param str flashdutyIntegrationKey: Notification option for ``type`` :attr:`~.NotificationType.FLASHDUTY`. :param str, optional freemobileUser: Notification option for ``type`` :attr:`~.NotificationType.FREEMOBILE`. :param str, optional freemobilePass: Notification option for ``type`` :attr:`~.NotificationType.FREEMOBILE`. :param str, optional goAlertBaseURL: Notification option for ``type`` :attr:`~.NotificationType.GOALERT`. @@ -134,6 +177,9 @@ def notification_docstring(mode) -> str: :param str mattermostchannel: Notification option for ``type`` :attr:`~.NotificationType.MATTERMOST`. :param str mattermosticonemo: Notification option for ``type`` :attr:`~.NotificationType.MATTERMOST`. :param str mattermosticonurl: Notification option for ``type`` :attr:`~.NotificationType.MATTERMOST`. + :param str, optional sender: Notification option for ``type`` :attr:`~.NotificationType.NOSTR`. + :param str, optional recipients: Notification option for ``type`` :attr:`~.NotificationType.NOSTR`. + :param str, optional relays: Notification option for ``type`` :attr:`~.NotificationType.NOSTR`. :param str ntfyAuthenticationMethod: Notification option for ``type`` :attr:`~.NotificationType.NTFY`. Authentication Method. :param str ntfyusername: Notification option for ``type`` :attr:`~.NotificationType.NTFY`. :param str ntfypassword: Notification option for ``type`` :attr:`~.NotificationType.NTFY`. @@ -192,6 +238,7 @@ def notification_docstring(mode) -> str: - ``4``: SMS SPEED - Highest priority in system. Very quick and reliable but costly (about twice of SMS FULL price). :param str promosmsSenderName: Notification option for ``type`` :attr:`~.NotificationType.PROMOSMS`. :param str, optional pushbulletAccessToken: Notification option for ``type`` :attr:`~.NotificationType.PUSHBULLET`. + :param str pushdeerServer: Notification option for ``type`` :attr:`~.NotificationType.PUSHDEER`. :param str, optional pushdeerKey: Notification option for ``type`` :attr:`~.NotificationType.PUSHDEER`. :param str, optional pushoveruserkey: Notification option for ``type`` :attr:`~.NotificationType.PUSHOVER`. :param str, optional pushoverapptoken: Notification option for ``type`` :attr:`~.NotificationType.PUSHOVER`. @@ -214,10 +261,16 @@ def notification_docstring(mode) -> str: :param str, optional signalNumber: Notification option for ``type`` :attr:`~.NotificationType.SIGNAL`. :param str, optional signalRecipients: Notification option for ``type`` :attr:`~.NotificationType.SIGNAL`. :param str, optional signalURL: Notification option for ``type`` :attr:`~.NotificationType.SIGNAL`. + :param bool slackchannelnotify: Notification option for ``type`` :attr:`~.NotificationType.SLACK`. :param str slackchannel: Notification option for ``type`` :attr:`~.NotificationType.SLACK`. :param str slackusername: Notification option for ``type`` :attr:`~.NotificationType.SLACK`. :param str slackiconemo: Notification option for ``type`` :attr:`~.NotificationType.SLACK`. :param str, optional slackwebhookURL: Notification option for ``type`` :attr:`~.NotificationType.SLACK`. + :param str smscTranslit: Notification option for ``type`` :attr:`~.NotificationType.SMSC`. + :param str, optional smscLogin: Notification option for ``type`` :attr:`~.NotificationType.SMSC`. + :param str, optional smscPassword: Notification option for ``type`` :attr:`~.NotificationType.SMSC`. + :param str, optional smscToNumber: Notification option for ``type`` :attr:`~.NotificationType.SMSC`. + :param str smscSenderName: Notification option for ``type`` :attr:`~.NotificationType.SMSC`. :param bool smseagleEncoding: Notification option for ``type`` :attr:`~.NotificationType.SMSEAGLE`. True to send messages in unicode. :param int smseaglePriority: Notification option for ``type`` :attr:`~.NotificationType.SMSEAGLE`. Message priority (0-9, default = 0). :param str smseagleRecipientType: Notification option for ``type`` :attr:`~.NotificationType.SMSEAGLE`. Recipient type. @@ -275,10 +328,12 @@ def notification_docstring(mode) -> str: :param str telegramMessageThreadID: Notification option for ``type`` :attr:`~.NotificationType.TELEGRAM`. :param str, optional telegramBotToken: Notification option for ``type`` :attr:`~.NotificationType.TELEGRAM`. :param str, optional twilioAccountSID: Notification option for ``type`` :attr:`~.NotificationType.TWILIO`. Account SID. + :param str twilioApiKey: Notification option for ``type`` :attr:`~.NotificationType.TWILIO`. :param str, optional twilioAuthToken: Notification option for ``type`` :attr:`~.NotificationType.TWILIO`. Auth Token. :param str, optional twilioToNumber: Notification option for ``type`` :attr:`~.NotificationType.TWILIO`. To Number. :param str, optional twilioFromNumber: Notification option for ``type`` :attr:`~.NotificationType.TWILIO`. From Number. :param str, optional webhookContentType: Notification option for ``type`` :attr:`~.NotificationType.WEBHOOK`. + :param str webhookCustomBody: Notification option for ``type`` :attr:`~.NotificationType.WEBHOOK`. :param str webhookAdditionalHeaders: Notification option for ``type`` :attr:`~.NotificationType.WEBHOOK`. :param str, optional webhookURL: Notification option for ``type`` :attr:`~.NotificationType.WEBHOOK`. :param str, optional weComBotKey: Notification option for ``type`` :attr:`~.NotificationType.WECOM`. diff --git a/uptime_kuma_api/monitor_type.py b/uptime_kuma_api/monitor_type.py index 380c994..b967bed 100644 --- a/uptime_kuma_api/monitor_type.py +++ b/uptime_kuma_api/monitor_type.py @@ -3,6 +3,9 @@ from enum import Enum class MonitorType(str, Enum): """Enumerate monitor types.""" + + GROUP = "group" + """Group""" HTTP = "http" """HTTP(s)""" @@ -16,6 +19,9 @@ class MonitorType(str, Enum): KEYWORD = "keyword" """HTTP(s) - Keyword""" + JSON_QUERY = "json-query" + """HTTP(s) - Json Query""" + GRPC_KEYWORD = "grpc-keyword" """gRPC(s) - Keyword""" @@ -25,6 +31,9 @@ class MonitorType(str, Enum): DOCKER = "docker" """Docker Container""" + REAL_BROWSER = "real-browser" + """HTTP(s) - Browser Engine (Chrome/Chromium)""" + PUSH = "push" """Push""" @@ -37,6 +46,9 @@ class MonitorType(str, Enum): MQTT = "mqtt" """MQTT""" + KAFKA_PRODUCER = "kafka-producer" + """Kafka Producer""" + SQLSERVER = "sqlserver" """Microsoft SQL Server""" @@ -55,5 +67,5 @@ class MonitorType(str, Enum): REDIS = "redis" """Redis""" - GROUP = "group" - """Group""" + TAILSCALE_PING = "tailscale-ping" + """Tailscale Ping""" diff --git a/uptime_kuma_api/notification_providers.py b/uptime_kuma_api/notification_providers.py index ccb239c..f9c6bdb 100644 --- a/uptime_kuma_api/notification_providers.py +++ b/uptime_kuma_api/notification_providers.py @@ -31,6 +31,9 @@ class NotificationType(str, Enum): FEISHU = "Feishu" """Feishu""" + FLASHDUTY = "FlashDuty" + """FlashDuty""" + FREEMOBILE = "FreeMobile" """FreeMobile (mobile.free.fr)""" @@ -67,6 +70,9 @@ class NotificationType(str, Enum): MATTERMOST = "mattermost" """Mattermost""" + NOSTR = "nostr" + """Nostr""" + NTFY = "ntfy" """Ntfy""" @@ -115,6 +121,9 @@ class NotificationType(str, Enum): SLACK = "slack" """Slack""" + SMSC = "smsc" + """SMSC""" + SMSEAGLE = "SMSEagle" """SMSEagle""" @@ -200,6 +209,10 @@ notification_provider_options = { NotificationType.FEISHU: dict( feishuWebHookUrl=dict(type="str", required=True), ), + NotificationType.FLASHDUTY: dict( + flashdutySeverity=dict(type="str", required=True), + flashdutyIntegrationKey=dict(type="str", required=False), + ), NotificationType.FREEMOBILE: dict( freemobileUser=dict(type="str", required=True), freemobilePass=dict(type="str", required=True), @@ -258,6 +271,11 @@ notification_provider_options = { mattermosticonemo=dict(type="str", required=False), mattermosticonurl=dict(type="str", required=False), ), + NotificationType.NOSTR: dict( + sender=dict(type="str", required=True), + recipients=dict(type="str", required=True), + relays=dict(type="str", required=True), + ), NotificationType.NTFY: dict( ntfyAuthenticationMethod=dict(type="str", required=False), ntfyusername=dict(type="str", required=False), @@ -310,6 +328,7 @@ notification_provider_options = { pushbulletAccessToken=dict(type="str", required=True), ), NotificationType.PUSHDEER: dict( + pushdeerServer=dict(type="str", required=False), pushdeerKey=dict(type="str", required=True), ), NotificationType.PUSHOVER: dict( @@ -346,11 +365,19 @@ notification_provider_options = { signalURL=dict(type="str", required=True), ), NotificationType.SLACK: dict( + slackchannelnotify=dict(type="bool", required=False), slackchannel=dict(type="str", required=False), slackusername=dict(type="str", required=False), slackiconemo=dict(type="str", required=False), slackwebhookURL=dict(type="str", required=True), ), + NotificationType.SMSC: dict( + smscTranslit=dict(type="str", required=False), + smscLogin=dict(type="str", required=True), + smscPassword=dict(type="str", required=True), + smscToNumber=dict(type="str", required=True), + smscSenderName=dict(type="str", required=False), + ), NotificationType.SMSEAGLE: dict( smseagleEncoding=dict(type="bool", required=False), smseaglePriority=dict(type="int", required=False), @@ -409,12 +436,14 @@ notification_provider_options = { ), NotificationType.TWILIO: dict( twilioAccountSID=dict(type="str", required=True), + twilioApiKey=dict(type="str", required=False), twilioAuthToken=dict(type="str", required=True), twilioToNumber=dict(type="str", required=True), twilioFromNumber=dict(type="str", required=True), ), NotificationType.WEBHOOK: dict( webhookContentType=dict(type="str", required=True), + webhookCustomBody=dict(type="str", required=False), webhookAdditionalHeaders=dict(type="str", required=False), webhookURL=dict(type="str", required=True), ),