From 6c1e1b4737a5aa5ccef4512bb403e7c61997305e Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 1 Sep 2023 14:48:20 -0400 Subject: [PATCH 01/18] Add Arcadia Notifications to Front end --- app/classes/models/users.py | 2 + app/classes/shared/helpers.py | 16 ++- app/classes/web/routes/api/api_handlers.py | 6 + .../routes/api/crafty/announcements/index.py | 106 ++++++++++++++++ app/frontend/templates/notify.html | 120 ++++++++++++++++-- app/migrations/20230901_user_notif.py | 16 +++ 6 files changed, 248 insertions(+), 18 deletions(-) create mode 100644 app/classes/web/routes/api/crafty/announcements/index.py create mode 100644 app/migrations/20230901_user_notif.py diff --git a/app/classes/models/users.py b/app/classes/models/users.py index b0612017..ccd8f1b0 100644 --- a/app/classes/models/users.py +++ b/app/classes/models/users.py @@ -45,6 +45,7 @@ class Users(BaseModel): manager = IntegerField(default=None, null=True) pfp = CharField(default="/static/assets/images/faces-clipart/pic-3.png") theme = CharField(default="default") + cleared_notifs = CharField(default="default") class Meta: table_name = "users" @@ -171,6 +172,7 @@ class HelperUsers: "roles": [], "servers": [], "support_logs": "", + "cleared_notifs": "", } user = model_to_dict(Users.get(Users.user_id == user_id)) diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 489115ae..59c1ae3a 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -581,12 +581,16 @@ class Helpers: @staticmethod def get_announcements(): - data = ( - '[{"id":"1","date":"Unknown",' - '"title":"Error getting Announcements",' - '"desc":"Error getting Announcements","link":""}]' - ) - + data = [] + data = [ + { + "id": "4db1a43b-d451-4abc-bb10-5a45676d95f7", + "date": "Unknown", + "title": "Error getting Announcements", + "desc": "Error getting Announcements", + "link": "", + } + ] try: response = requests.get("https://craftycontrol.com/notify.json", timeout=2) data = json.loads(response.content) diff --git a/app/classes/web/routes/api/api_handlers.py b/app/classes/web/routes/api/api_handlers.py index 29ee02c5..c3558a65 100644 --- a/app/classes/web/routes/api/api_handlers.py +++ b/app/classes/web/routes/api/api_handlers.py @@ -40,6 +40,7 @@ from app.classes.web.routes.api.users.user.permissions import ( ) from app.classes.web.routes.api.users.user.pfp import ApiUsersUserPfpHandler from app.classes.web.routes.api.users.user.public import ApiUsersUserPublicHandler +from app.classes.web.routes.api.crafty.announcements.index import ApiAnnounceIndexHandler def api_handlers(handler_args): @@ -55,6 +56,11 @@ def api_handlers(handler_args): ApiAuthInvalidateTokensHandler, handler_args, ), + ( + r"/api/v2/crafty/announcements/?", + ApiAnnounceIndexHandler, + handler_args, + ), # User routes ( r"/api/v2/users/?", diff --git a/app/classes/web/routes/api/crafty/announcements/index.py b/app/classes/web/routes/api/crafty/announcements/index.py new file mode 100644 index 00000000..a7012466 --- /dev/null +++ b/app/classes/web/routes/api/crafty/announcements/index.py @@ -0,0 +1,106 @@ +import logging +import json +from jsonschema import ValidationError, validate +from app.classes.web.base_api_handler import BaseApiHandler + +logger = logging.getLogger(__name__) + +notif_schema = { + "type": "object", + "properties": { + "id": {"type": "string"}, + }, + "additionalProperties": False, + "minProperties": 1, +} + + +class ApiAnnounceIndexHandler(BaseApiHandler): + def get(self): + auth_data = self.authenticate_user() + if not auth_data: + return + ( + _, + _exec_user_crafty_permissions, + _, + _, + _user, + ) = auth_data + + data = self.helper.get_announcements() + cleared = str( + self.controller.users.get_user_by_id(auth_data[4]["user_id"])[ + "cleared_notifs" + ] + ).split(",") + res = [d.get("id", None) for d in data] + # remove notifs that are no longer in Crafty. + for item in cleared[:]: + if item not in res: + cleared.remove(item) + if len(cleared) > 0: + for item in data[:]: + if item["id"] in cleared: + data.remove(item) + + self.finish_json( + 200, + { + "status": "ok", + "data": data, + }, + ) + + def post(self): + auth_data = self.authenticate_user() + if not auth_data: + return + ( + _, + _exec_user_crafty_permissions, + _, + _, + _user, + ) = auth_data + print(self.request) + try: + data = json.loads(self.request.body) + print(data) + except json.decoder.JSONDecodeError as e: + return self.finish_json( + 400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)} + ) + + try: + validate(data, notif_schema) + except ValidationError as e: + return self.finish_json( + 400, + { + "status": "error", + "error": "INVALID_JSON_SCHEMA", + "error_data": str(e), + }, + ) + announcements = self.helper.get_announcements() + res = [d.get("id", None) for d in announcements] + cleared_notifs = str( + self.controller.users.get_user_by_id(auth_data[4]["user_id"])[ + "cleared_notifs" + ] + ).split(",") + # remove notifs that are no longer in Crafty. + for item in cleared_notifs[:]: + if item not in res: + cleared_notifs.remove(item) + cleared_notifs.append(data["id"]) + updata = {"cleared_notifs": ",".join(cleared_notifs)} + self.controller.users.update_user(auth_data[4]["user_id"], updata) + self.finish_json( + 200, + { + "status": "ok", + "data": {}, + }, + ) diff --git a/app/frontend/templates/notify.html b/app/frontend/templates/notify.html index 9d41d17d..1600e0f5 100644 --- a/app/frontend/templates/notify.html +++ b/app/frontend/templates/notify.html @@ -1,27 +1,32 @@ + \ No newline at end of file diff --git a/app/migrations/20230901_user_notif.py b/app/migrations/20230901_user_notif.py new file mode 100644 index 00000000..eaefc159 --- /dev/null +++ b/app/migrations/20230901_user_notif.py @@ -0,0 +1,16 @@ +# Generated by database migrator +import peewee + + +def migrate(migrator, database, **kwargs): + migrator.add_columns("users", cleared_notifs=peewee.CharField(default="")) + """ + Write your migrations here. + """ + + +def rollback(migrator, database, **kwargs): + migrator.drop_columns("users", ["cleared_notifs"]) + """ + Write your rollback migrations here. + """ From 02df6de6d5337647b0a48673cf8882b901bea453 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 1 Sep 2023 14:54:38 -0400 Subject: [PATCH 02/18] Remove prints. Remove test notif --- app/classes/shared/helpers.py | 9 --------- app/classes/web/routes/api/crafty/announcements/index.py | 2 -- 2 files changed, 11 deletions(-) diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 59c1ae3a..9c74135d 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -582,15 +582,6 @@ class Helpers: @staticmethod def get_announcements(): data = [] - data = [ - { - "id": "4db1a43b-d451-4abc-bb10-5a45676d95f7", - "date": "Unknown", - "title": "Error getting Announcements", - "desc": "Error getting Announcements", - "link": "", - } - ] try: response = requests.get("https://craftycontrol.com/notify.json", timeout=2) data = json.loads(response.content) diff --git a/app/classes/web/routes/api/crafty/announcements/index.py b/app/classes/web/routes/api/crafty/announcements/index.py index a7012466..79899e9e 100644 --- a/app/classes/web/routes/api/crafty/announcements/index.py +++ b/app/classes/web/routes/api/crafty/announcements/index.py @@ -63,10 +63,8 @@ class ApiAnnounceIndexHandler(BaseApiHandler): _, _user, ) = auth_data - print(self.request) try: data = json.loads(self.request.body) - print(data) except json.decoder.JSONDecodeError as e: return self.finish_json( 400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)} From f03e8d48a24e3ac5ac5036868e7e467fe701162f Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 1 Sep 2023 17:04:56 -0400 Subject: [PATCH 03/18] Appease the linter --- app/classes/web/routes/api/api_handlers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/classes/web/routes/api/api_handlers.py b/app/classes/web/routes/api/api_handlers.py index c3558a65..97147c92 100644 --- a/app/classes/web/routes/api/api_handlers.py +++ b/app/classes/web/routes/api/api_handlers.py @@ -40,7 +40,9 @@ from app.classes.web.routes.api.users.user.permissions import ( ) from app.classes.web.routes.api.users.user.pfp import ApiUsersUserPfpHandler from app.classes.web.routes.api.users.user.public import ApiUsersUserPublicHandler -from app.classes.web.routes.api.crafty.announcements.index import ApiAnnounceIndexHandler +from app.classes.web.routes.api.crafty.announcements.index import ( + ApiAnnounceIndexHandler, +) def api_handlers(handler_args): From 9d870f06113a85d22a26295726116d92f171e0c2 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 1 Sep 2023 20:15:33 -0400 Subject: [PATCH 04/18] Check for valid uuid --- .../web/routes/api/crafty/announcements/index.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/classes/web/routes/api/crafty/announcements/index.py b/app/classes/web/routes/api/crafty/announcements/index.py index 79899e9e..12430725 100644 --- a/app/classes/web/routes/api/crafty/announcements/index.py +++ b/app/classes/web/routes/api/crafty/announcements/index.py @@ -1,5 +1,6 @@ import logging import json +import uuid from jsonschema import ValidationError, validate from app.classes.web.base_api_handler import BaseApiHandler @@ -92,7 +93,11 @@ class ApiAnnounceIndexHandler(BaseApiHandler): for item in cleared_notifs[:]: if item not in res: cleared_notifs.remove(item) - cleared_notifs.append(data["id"]) + if is_valid_uuid(data["id"]): + cleared_notifs.append(data["id"]) + else: + self.finish_json(200, {"status": "error", "error": "INVALID_DATA"}) + return updata = {"cleared_notifs": ",".join(cleared_notifs)} self.controller.users.update_user(auth_data[4]["user_id"], updata) self.finish_json( @@ -102,3 +107,12 @@ class ApiAnnounceIndexHandler(BaseApiHandler): "data": {}, }, ) + + +def is_valid_uuid(value): + try: + uuid.UUID(str(value)) + + return True + except ValueError: + return False From 5266e6e098bec5565d02855b6b9b79e47e155631 Mon Sep 17 00:00:00 2001 From: Andrew Date: Fri, 1 Sep 2023 20:38:10 -0400 Subject: [PATCH 05/18] style ul --- app/frontend/templates/notify.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/frontend/templates/notify.html b/app/frontend/templates/notify.html index 1600e0f5..9848e98a 100644 --- a/app/frontend/templates/notify.html +++ b/app/frontend/templates/notify.html @@ -8,7 +8,7 @@ {% end %} "> @@ -96,7 +96,8 @@ $("#notif-count").html(data.length); $("#announcements").html(text); } else { - $("#announcements").html("

No notifications

"); + $("#announcements").html(`

+

`); } $(".clear-button").on("click", function (event) { console.log("CLEAR BUTTON") @@ -108,7 +109,8 @@ localStorage.setItem("notif-count", notif_count); $("#notif-count").html(notif_count); } else { - $("#announcements").html("

No notifications

") + $("#announcements").html(`

+

`) $("#notif-count").html(""); } From ff1ba23830c1c585bc23a9770bb6e5d389fcba2d Mon Sep 17 00:00:00 2001 From: Andrew Date: Sat, 2 Sep 2023 19:35:16 -0400 Subject: [PATCH 06/18] Add updates to notifs --- app/classes/shared/helpers.py | 5 +++-- app/classes/shared/tasks.py | 11 +++++++++++ .../web/routes/api/crafty/announcements/index.py | 13 +++---------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 9c74135d..0f36886a 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -579,8 +579,7 @@ class Helpers: return version_data - @staticmethod - def get_announcements(): + def get_announcements(self): data = [] try: response = requests.get("https://craftycontrol.com/notify.json", timeout=2) @@ -588,6 +587,8 @@ class Helpers: except Exception as e: logger.error(f"Failed to fetch notifications with error: {e}") + if self.update_available: + data.append(self.update_available) return data def get_version_string(self): diff --git a/app/classes/shared/tasks.py b/app/classes/shared/tasks.py index acdc1cac..4d878066 100644 --- a/app/classes/shared/tasks.py +++ b/app/classes/shared/tasks.py @@ -726,12 +726,23 @@ class TasksManager: def check_for_updates(self): logger.info("Checking for Crafty updates...") self.helper.update_available = self.helper.check_remote_version() + remote = self.helper.update_available if self.helper.update_available: logger.info(f"Found new version {self.helper.update_available}") else: logger.info( "No updates found! You are on the most up to date Crafty version." ) + if self.helper.update_available: + self.helper.update_available = { + "id": str(remote), + "title": f"{remote} Update Available", + "date": "", + "desc": "Instructions for updating can be found" + " by clicking this notification.", + "link": "https://docs.craftycontrol.com/pages/" + "getting-started/installation/linux/?h=updating#updating-crafty", + } logger.info("Refreshing Gravatar PFPs...") for user in HelperUsers.get_all_users(): if user.email: diff --git a/app/classes/web/routes/api/crafty/announcements/index.py b/app/classes/web/routes/api/crafty/announcements/index.py index 12430725..ff274256 100644 --- a/app/classes/web/routes/api/crafty/announcements/index.py +++ b/app/classes/web/routes/api/crafty/announcements/index.py @@ -40,6 +40,8 @@ class ApiAnnounceIndexHandler(BaseApiHandler): for item in cleared[:]: if item not in res: cleared.remove(item) + updata = {"cleared_notifs": ",".join(cleared)} + self.controller.users.update_user(auth_data[4]["user_id"], updata) if len(cleared) > 0: for item in data[:]: if item["id"] in cleared: @@ -93,7 +95,7 @@ class ApiAnnounceIndexHandler(BaseApiHandler): for item in cleared_notifs[:]: if item not in res: cleared_notifs.remove(item) - if is_valid_uuid(data["id"]): + if str(data["id"]) in str(res): cleared_notifs.append(data["id"]) else: self.finish_json(200, {"status": "error", "error": "INVALID_DATA"}) @@ -107,12 +109,3 @@ class ApiAnnounceIndexHandler(BaseApiHandler): "data": {}, }, ) - - -def is_valid_uuid(value): - try: - uuid.UUID(str(value)) - - return True - except ValueError: - return False From 5e4560c4610a1b9f50bb2ae55aec59b908638fe2 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sat, 2 Sep 2023 19:35:25 -0400 Subject: [PATCH 07/18] Remove from ajax handler --- app/classes/web/ajax_handler.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/classes/web/ajax_handler.py b/app/classes/web/ajax_handler.py index efe8d2fa..1c703ecc 100644 --- a/app/classes/web/ajax_handler.py +++ b/app/classes/web/ajax_handler.py @@ -83,11 +83,6 @@ class AjaxHandler(BaseHandler): except Exception as e: logger.warning(f"Skipping Log Line due to error: {e}") - elif page == "announcements": - data = Helpers.get_announcements() - page_data["notify_data"] = data - self.render_page("ajax/notify.html", page_data) - elif page == "get_zip_tree": path = self.get_argument("path", None) From 859a1737f350566db8d730f46828c7d2397f8161 Mon Sep 17 00:00:00 2001 From: Andrew Date: Sun, 3 Sep 2023 15:26:04 -0400 Subject: [PATCH 08/18] Change link to json --- app/classes/shared/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 0f36886a..5955548e 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -582,7 +582,7 @@ class Helpers: def get_announcements(self): data = [] try: - response = requests.get("https://craftycontrol.com/notify.json", timeout=2) + response = requests.get("https://craftycontrol.com/notify", timeout=2) data = json.loads(response.content) except Exception as e: logger.error(f"Failed to fetch notifications with error: {e}") From 92a9fffafcd43dda9a97d44dcd5533aa996978cc Mon Sep 17 00:00:00 2001 From: Andrew Date: Sun, 3 Sep 2023 15:26:13 -0400 Subject: [PATCH 09/18] Make update notif point to release notes --- app/classes/shared/tasks.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/classes/shared/tasks.py b/app/classes/shared/tasks.py index 4d878066..a117452f 100644 --- a/app/classes/shared/tasks.py +++ b/app/classes/shared/tasks.py @@ -738,10 +738,8 @@ class TasksManager: "id": str(remote), "title": f"{remote} Update Available", "date": "", - "desc": "Instructions for updating can be found" - " by clicking this notification.", - "link": "https://docs.craftycontrol.com/pages/" - "getting-started/installation/linux/?h=updating#updating-crafty", + "desc": "Release notes are available by clicking this notification.", + "link": "https://gitlab.com/crafty-controller/crafty-4/-/releases", } logger.info("Refreshing Gravatar PFPs...") for user in HelperUsers.get_all_users(): From 13539d83a9bcb03fc89d41bf27cc08af6994bd2f Mon Sep 17 00:00:00 2001 From: Andrew Date: Sun, 3 Sep 2023 17:27:09 -0400 Subject: [PATCH 10/18] add style --- app/frontend/templates/notify.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/frontend/templates/notify.html b/app/frontend/templates/notify.html index 9848e98a..4348fad5 100644 --- a/app/frontend/templates/notify.html +++ b/app/frontend/templates/notify.html @@ -89,7 +89,7 @@ console.log(data) let text = ""; for (i = 0; i < data.length; i++) { - text += `
  • ${data[i].title}

    ${data[i].date}

    ${data[i].desc}

  • ` + text += `
  • ${data[i].title}

    ${data[i].date}

    ${data[i].desc}

  • ` } if (data.length > 0) { localStorage.setItem("notif-count", data.length); From 98720aa072181d202508caba9ab96f1c2ca4af4c Mon Sep 17 00:00:00 2001 From: amcmanu3 Date: Mon, 4 Sep 2023 17:30:47 -0400 Subject: [PATCH 11/18] Poll notifications on an hourly schedule --- app/classes/shared/helpers.py | 7 ++++++- app/classes/shared/tasks.py | 8 ++++++++ app/frontend/templates/notify.html | 20 +++++++++++++++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 5955548e..7cc87b68 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -82,6 +82,7 @@ class Helpers: self.translation = Translation(self) self.update_available = False self.ignored_names = ["crafty_managed.txt", "db_stats"] + self.announcements = [] @staticmethod def auto_installer_fix(ex): @@ -579,7 +580,7 @@ class Helpers: return version_data - def get_announcements(self): + def refresh_announcements(self): data = [] try: response = requests.get("https://craftycontrol.com/notify", timeout=2) @@ -587,6 +588,10 @@ class Helpers: except Exception as e: logger.error(f"Failed to fetch notifications with error: {e}") + self.announcements = data + + def get_announcements(self): + data = self.announcements if self.update_available: data.append(self.update_available) return data diff --git a/app/classes/shared/tasks.py b/app/classes/shared/tasks.py index a117452f..8d27ddb2 100644 --- a/app/classes/shared/tasks.py +++ b/app/classes/shared/tasks.py @@ -200,6 +200,14 @@ class TasksManager: id="update_watcher", start_date=datetime.datetime.now(), ) + self.helper.refresh_announcements() + self.scheduler.add_job( + self.helper.refresh_announcements, + "interval", + hours=1, + id="annoucement_watcher", + start_date=datetime.datetime.now(), + ) # self.scheduler.add_job( # self.scheduler.print_jobs, "interval", seconds=10, id="-1" # ) diff --git a/app/frontend/templates/notify.html b/app/frontend/templates/notify.html index 4348fad5..f8c3ad9e 100644 --- a/app/frontend/templates/notify.html +++ b/app/frontend/templates/notify.html @@ -7,7 +7,8 @@ text-danger {% end %} "> -