From 754e77c9cbd2cd23ad4dde76bfa051741f99464b Mon Sep 17 00:00:00 2001 From: Michael Genson <71845777+michael-genson@users.noreply.github.com> Date: Wed, 15 Jun 2022 14:49:42 -0500 Subject: [PATCH] feat: extend Apprise JSON notification functionality with programmatic data (#1355) * Fixed incorrect generic deleted notification text * Added custom "event_source" header for json notifs * Added internal reference data to event notifs * Added event listeners to shopping list items * Fixed type issues * moved JSON event source k:v pairs to message body * added hook for all supported custom endpoints fixed bug that excluded non-custom notification types * created event_source class to replace loosely-typed dict * fixed silent error when dispatching a null task * moved url updates to static function * added unit tests for event_source url manipulation * removed array from event bus (it's unsupported) --- mealie/lang/messages/af-ZA.json | 2 +- mealie/lang/messages/ar-SA.json | 2 +- mealie/lang/messages/bg-BG.json | 2 +- mealie/lang/messages/ca-ES.json | 2 +- mealie/lang/messages/cs-CZ.json | 2 +- mealie/lang/messages/da-DK.json | 2 +- mealie/lang/messages/de-DE.json | 2 +- mealie/lang/messages/el-GR.json | 2 +- mealie/lang/messages/en-GB.json | 2 +- mealie/lang/messages/en-US.json | 2 +- mealie/lang/messages/es-ES.json | 2 +- mealie/lang/messages/fi-FI.json | 2 +- mealie/lang/messages/fr-CA.json | 2 +- mealie/lang/messages/fr-FR.json | 2 +- mealie/lang/messages/he-IL.json | 2 +- mealie/lang/messages/hu-HU.json | 2 +- mealie/lang/messages/it-IT.json | 2 +- mealie/lang/messages/ja-JP.json | 2 +- mealie/lang/messages/ko-KR.json | 2 +- mealie/lang/messages/nl-NL.json | 2 +- mealie/lang/messages/no-NO.json | 2 +- mealie/lang/messages/pl-PL.json | 2 +- mealie/lang/messages/pt-BR.json | 2 +- mealie/lang/messages/pt-PT.json | 2 +- mealie/lang/messages/ro-RO.json | 2 +- mealie/lang/messages/ru-RU.json | 2 +- mealie/lang/messages/sk-SK.json | 2 +- mealie/lang/messages/sr-SP.json | 2 +- mealie/lang/messages/sv-SE.json | 2 +- mealie/lang/messages/tr-TR.json | 2 +- mealie/lang/messages/uk-UA.json | 2 +- mealie/lang/messages/vi-VN.json | 2 +- mealie/lang/messages/zh-CN.json | 2 +- mealie/lang/messages/zh-TW.json | 2 +- mealie/routes/groups/controller_cookbooks.py | 5 +- .../groups/controller_shopping_lists.py | 115 ++++++++++++++++-- .../organizers/controller_categories.py | 5 +- mealie/routes/organizers/controller_tags.py | 5 +- mealie/routes/recipe/recipe_crud_routes.py | 38 +++++- .../event_bus_service/event_bus_service.py | 68 +++++++++-- .../test_group_notifications.py | 42 ++++++- tests/utils/factories.py | 4 + 42 files changed, 296 insertions(+), 54 deletions(-) diff --git a/mealie/lang/messages/af-ZA.json b/mealie/lang/messages/af-ZA.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/af-ZA.json +++ b/mealie/lang/messages/af-ZA.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/ar-SA.json b/mealie/lang/messages/ar-SA.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/ar-SA.json +++ b/mealie/lang/messages/ar-SA.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/bg-BG.json b/mealie/lang/messages/bg-BG.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/bg-BG.json +++ b/mealie/lang/messages/bg-BG.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/ca-ES.json b/mealie/lang/messages/ca-ES.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/ca-ES.json +++ b/mealie/lang/messages/ca-ES.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/cs-CZ.json b/mealie/lang/messages/cs-CZ.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/cs-CZ.json +++ b/mealie/lang/messages/cs-CZ.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/da-DK.json b/mealie/lang/messages/da-DK.json index 89a866bda4ed..d9010d194e6d 100644 --- a/mealie/lang/messages/da-DK.json +++ b/mealie/lang/messages/da-DK.json @@ -17,6 +17,6 @@ "generic-updated": "{name} blev opdateret", "generic-created-with-url": "{name} er oprettet, {url}", "generic-updated-with-url": "{name} er blevet opdateret, {url}", - "generic-deleted": "{name} er oprettet" + "generic-deleted": "{name} er slettet" } } diff --git a/mealie/lang/messages/de-DE.json b/mealie/lang/messages/de-DE.json index dcdb3c72acdc..0b082c2e9281 100644 --- a/mealie/lang/messages/de-DE.json +++ b/mealie/lang/messages/de-DE.json @@ -17,6 +17,6 @@ "generic-updated": "{name} wurde aktualisiert", "generic-created-with-url": "{name} wurde erstellt, {url}", "generic-updated-with-url": "{name} wurde aktualisiert, {url}", - "generic-deleted": "{name} wurde erstellt" + "generic-deleted": "{name} wurde gelöscht" } } diff --git a/mealie/lang/messages/el-GR.json b/mealie/lang/messages/el-GR.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/el-GR.json +++ b/mealie/lang/messages/el-GR.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/en-GB.json b/mealie/lang/messages/en-GB.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/en-GB.json +++ b/mealie/lang/messages/en-GB.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/en-US.json b/mealie/lang/messages/en-US.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/en-US.json +++ b/mealie/lang/messages/en-US.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/es-ES.json b/mealie/lang/messages/es-ES.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/es-ES.json +++ b/mealie/lang/messages/es-ES.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/fi-FI.json b/mealie/lang/messages/fi-FI.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/fi-FI.json +++ b/mealie/lang/messages/fi-FI.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/fr-CA.json b/mealie/lang/messages/fr-CA.json index 959e2969651e..938662437497 100644 --- a/mealie/lang/messages/fr-CA.json +++ b/mealie/lang/messages/fr-CA.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/fr-FR.json b/mealie/lang/messages/fr-FR.json index 16e797ba02d5..29ede8772527 100644 --- a/mealie/lang/messages/fr-FR.json +++ b/mealie/lang/messages/fr-FR.json @@ -17,6 +17,6 @@ "generic-updated": "{name} a été mis à jour", "generic-created-with-url": "{name} a été créé, {url}", "generic-updated-with-url": "{name} a été mis à jour, {url}", - "generic-deleted": "{name} a été créé" + "generic-deleted": "{name} a été supprimée" } } diff --git a/mealie/lang/messages/he-IL.json b/mealie/lang/messages/he-IL.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/he-IL.json +++ b/mealie/lang/messages/he-IL.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/hu-HU.json b/mealie/lang/messages/hu-HU.json index 9beb7a5cb717..a594ce909154 100644 --- a/mealie/lang/messages/hu-HU.json +++ b/mealie/lang/messages/hu-HU.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/it-IT.json b/mealie/lang/messages/it-IT.json index 6404559ef64a..1549746cbf6d 100644 --- a/mealie/lang/messages/it-IT.json +++ b/mealie/lang/messages/it-IT.json @@ -17,6 +17,6 @@ "generic-updated": "{name} è stato aggiornato", "generic-created-with-url": "{name} è stato creato, {url}", "generic-updated-with-url": "{name} è stato aggiornato, {url}", - "generic-deleted": "{name} è stato creato" + "generic-deleted": "{name} è stato cancellato" } } diff --git a/mealie/lang/messages/ja-JP.json b/mealie/lang/messages/ja-JP.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/ja-JP.json +++ b/mealie/lang/messages/ja-JP.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/ko-KR.json b/mealie/lang/messages/ko-KR.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/ko-KR.json +++ b/mealie/lang/messages/ko-KR.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/nl-NL.json b/mealie/lang/messages/nl-NL.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/nl-NL.json +++ b/mealie/lang/messages/nl-NL.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/no-NO.json b/mealie/lang/messages/no-NO.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/no-NO.json +++ b/mealie/lang/messages/no-NO.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/pl-PL.json b/mealie/lang/messages/pl-PL.json index 2f59625ba53b..549d3571b06e 100644 --- a/mealie/lang/messages/pl-PL.json +++ b/mealie/lang/messages/pl-PL.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/pt-BR.json b/mealie/lang/messages/pt-BR.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/pt-BR.json +++ b/mealie/lang/messages/pt-BR.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/pt-PT.json b/mealie/lang/messages/pt-PT.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/pt-PT.json +++ b/mealie/lang/messages/pt-PT.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/ro-RO.json b/mealie/lang/messages/ro-RO.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/ro-RO.json +++ b/mealie/lang/messages/ro-RO.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/ru-RU.json b/mealie/lang/messages/ru-RU.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/ru-RU.json +++ b/mealie/lang/messages/ru-RU.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/sk-SK.json b/mealie/lang/messages/sk-SK.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/sk-SK.json +++ b/mealie/lang/messages/sk-SK.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/sr-SP.json b/mealie/lang/messages/sr-SP.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/sr-SP.json +++ b/mealie/lang/messages/sr-SP.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/sv-SE.json b/mealie/lang/messages/sv-SE.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/sv-SE.json +++ b/mealie/lang/messages/sv-SE.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/tr-TR.json b/mealie/lang/messages/tr-TR.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/tr-TR.json +++ b/mealie/lang/messages/tr-TR.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/uk-UA.json b/mealie/lang/messages/uk-UA.json index 2b68bd974367..e3cc1d90b20c 100644 --- a/mealie/lang/messages/uk-UA.json +++ b/mealie/lang/messages/uk-UA.json @@ -17,6 +17,6 @@ "generic-updated": "{name} оновлено", "generic-created-with-url": "{name} створено, {url}", "generic-updated-with-url": "{name} оновлено, {url}", - "generic-deleted": "{name} створено" + "generic-deleted": "{name} видалено" } } diff --git a/mealie/lang/messages/vi-VN.json b/mealie/lang/messages/vi-VN.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/vi-VN.json +++ b/mealie/lang/messages/vi-VN.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/zh-CN.json b/mealie/lang/messages/zh-CN.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/zh-CN.json +++ b/mealie/lang/messages/zh-CN.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/lang/messages/zh-TW.json b/mealie/lang/messages/zh-TW.json index f60caf6acde3..20b0ba283154 100644 --- a/mealie/lang/messages/zh-TW.json +++ b/mealie/lang/messages/zh-TW.json @@ -17,6 +17,6 @@ "generic-updated": "{name} was updated", "generic-created-with-url": "{name} has been created, {url}", "generic-updated-with-url": "{name} has been updated, {url}", - "generic-deleted": "{name} has been created" + "generic-deleted": "{name} has been deleted" } } diff --git a/mealie/routes/groups/controller_cookbooks.py b/mealie/routes/groups/controller_cookbooks.py index 0260baa30643..d12a110a13ef 100644 --- a/mealie/routes/groups/controller_cookbooks.py +++ b/mealie/routes/groups/controller_cookbooks.py @@ -8,7 +8,7 @@ from mealie.routes._base import BaseUserController, controller from mealie.routes._base.mixins import HttpRepo from mealie.schema import mapper from mealie.schema.cookbook import CreateCookBook, ReadCookBook, RecipeCookBook, SaveCookBook, UpdateCookBook -from mealie.services.event_bus_service.event_bus_service import EventBusService +from mealie.services.event_bus_service.event_bus_service import EventBusService, EventSource from mealie.services.event_bus_service.message_types import EventTypes router = APIRouter(prefix="/groups/cookbooks", tags=["Groups: Cookbooks"]) @@ -53,6 +53,7 @@ class GroupCookbookController(BaseUserController): self.deps.acting_user.group_id, EventTypes.cookbook_created, msg=self.t("notifications.generic-created", name=val.name), + event_source=EventSource(event_type="create", item_type="cookbook", item_id=val.id, slug=val.slug), ) return val @@ -94,6 +95,7 @@ class GroupCookbookController(BaseUserController): self.deps.acting_user.group_id, EventTypes.cookbook_updated, msg=self.t("notifications.generic-updated", name=val.name), + event_source=EventSource(event_type="update", item_type="cookbook", item_id=val.id, slug=val.slug), ) return val @@ -106,5 +108,6 @@ class GroupCookbookController(BaseUserController): self.deps.acting_user.group_id, EventTypes.cookbook_deleted, msg=self.t("notifications.generic-deleted", name=val.name), + event_source=EventSource(event_type="delete", item_type="cookbook", item_id=val.id, slug=val.slug), ) return val diff --git a/mealie/routes/groups/controller_shopping_lists.py b/mealie/routes/groups/controller_shopping_lists.py index 6b8b823f5f59..a76781ec3a7b 100644 --- a/mealie/routes/groups/controller_shopping_lists.py +++ b/mealie/routes/groups/controller_shopping_lists.py @@ -19,7 +19,7 @@ from mealie.schema.group.group_shopping_list import ( from mealie.schema.mapper import cast from mealie.schema.query import GetAll from mealie.schema.response.responses import SuccessResponse -from mealie.services.event_bus_service.event_bus_service import EventBusService +from mealie.services.event_bus_service.event_bus_service import EventBusService, EventSource from mealie.services.event_bus_service.message_types import EventTypes from mealie.services.group_services.shopping_lists import ShoppingListService @@ -75,7 +75,25 @@ class ShoppingListItemController(BaseUserController): @item_router.post("", response_model=ShoppingListItemOut, status_code=201) def create_one(self, data: ShoppingListItemCreate): - return self.mixins.create_one(data) + shopping_list_item = self.mixins.create_one(data) + + if shopping_list_item: + self.event_bus.dispatch( + self.deps.acting_user.group_id, + EventTypes.shopping_list_updated, + msg=self.t( + "notifications.generic-created", + name=f"An item on shopping list {shopping_list_item.shopping_list_id}", + ), + event_source=EventSource( + event_type="create", + item_type="shopping-list-item", + item_id=shopping_list_item.id, + shopping_list_id=shopping_list_item.shopping_list_id, + ), + ) + + return shopping_list_item @item_router.get("/{item_id}", response_model=ShoppingListItemOut) def get_one(self, item_id: UUID4): @@ -83,11 +101,47 @@ class ShoppingListItemController(BaseUserController): @item_router.put("/{item_id}", response_model=ShoppingListItemOut) def update_one(self, item_id: UUID4, data: ShoppingListItemUpdate): - return self.mixins.update_one(data, item_id) + shopping_list_item = self.mixins.update_one(data, item_id) + + if shopping_list_item: + self.event_bus.dispatch( + self.deps.acting_user.group_id, + EventTypes.shopping_list_updated, + msg=self.t( + "notifications.generic-updated", + name=f"An item on shopping list {shopping_list_item.shopping_list_id}", + ), + event_source=EventSource( + event_type="update", + item_type="shopping-list-item", + item_id=shopping_list_item.id, + shopping_list_id=shopping_list_item.shopping_list_id, + ), + ) + + return shopping_list_item @item_router.delete("/{item_id}", response_model=ShoppingListItemOut) def delete_one(self, item_id: UUID4): - return self.mixins.delete_one(item_id) # type: ignore + shopping_list_item = self.mixins.delete_one(item_id) # type: ignore + + if shopping_list_item: + self.event_bus.dispatch( + self.deps.acting_user.group_id, + EventTypes.shopping_list_updated, + msg=self.t( + "notifications.generic-deleted", + name=f"An item on shopping list {shopping_list_item.shopping_list_id}", + ), + event_source=EventSource( + event_type="delete", + item_type="shopping-list-item", + item_id=shopping_list_item.id, + shopping_list_id=shopping_list_item.shopping_list_id, + ), + ) + + return shopping_list_item router = APIRouter(prefix="/groups/shopping/lists", tags=["Group: Shopping Lists"]) @@ -126,6 +180,11 @@ class ShoppingListController(BaseUserController): self.deps.acting_user.group_id, EventTypes.shopping_list_created, msg=self.t("notifications.generic-created", name=val.name), + event_source=EventSource( + event_type="create", + item_type="shopping-list", + item_id=val.id, + ), ) return val @@ -142,6 +201,11 @@ class ShoppingListController(BaseUserController): self.deps.acting_user.group_id, EventTypes.shopping_list_updated, msg=self.t("notifications.generic-updated", name=data.name), + event_source=EventSource( + event_type="update", + item_type="shopping-list", + item_id=data.id, + ), ) return data @@ -151,8 +215,13 @@ class ShoppingListController(BaseUserController): if data: self.event_bus.dispatch( self.deps.acting_user.group_id, - EventTypes.shopping_list_updated, + EventTypes.shopping_list_deleted, msg=self.t("notifications.generic-deleted", name=data.name), + event_source=EventSource( + event_type="delete", + item_type="shopping-list", + item_id=data.id, + ), ) return data @@ -161,8 +230,40 @@ class ShoppingListController(BaseUserController): @router.post("/{item_id}/recipe/{recipe_id}", response_model=ShoppingListOut) def add_recipe_ingredients_to_list(self, item_id: UUID4, recipe_id: UUID4): - return self.service.add_recipe_ingredients_to_list(item_id, recipe_id) + shopping_list = self.service.add_recipe_ingredients_to_list(item_id, recipe_id) + if shopping_list: + self.event_bus.dispatch( + self.deps.acting_user.group_id, + EventTypes.shopping_list_updated, + msg=self.t( + "notifications.generic-updated", + name=shopping_list.name, + ), + event_source=EventSource( + event_type="bulk-updated-items", + item_type="shopping-list", + item_id=shopping_list.id, + ), + ) + + return shopping_list @router.delete("/{item_id}/recipe/{recipe_id}", response_model=ShoppingListOut) def remove_recipe_ingredients_from_list(self, item_id: UUID4, recipe_id: UUID4): - return self.service.remove_recipe_ingredients_from_list(item_id, recipe_id) + shopping_list = self.service.remove_recipe_ingredients_from_list(item_id, recipe_id) + if shopping_list: + self.event_bus.dispatch( + self.deps.acting_user.group_id, + EventTypes.shopping_list_updated, + msg=self.t( + "notifications.generic-updated", + name=shopping_list.name, + ), + event_source=EventSource( + event_type="bulk-updated-items", + item_type="shopping-list", + item_id=shopping_list.id, + ), + ) + + return shopping_list diff --git a/mealie/routes/organizers/controller_categories.py b/mealie/routes/organizers/controller_categories.py index ea80dd0677bb..afdecfb5737c 100644 --- a/mealie/routes/organizers/controller_categories.py +++ b/mealie/routes/organizers/controller_categories.py @@ -10,7 +10,7 @@ from mealie.schema.recipe import CategoryIn, RecipeCategoryResponse from mealie.schema.recipe.recipe import RecipeCategory from mealie.schema.recipe.recipe_category import CategoryBase, CategorySave from mealie.services import urls -from mealie.services.event_bus_service.event_bus_service import EventBusService +from mealie.services.event_bus_service.event_bus_service import EventBusService, EventSource from mealie.services.event_bus_service.message_types import EventTypes router = APIRouter(prefix="/categories", tags=["Organizer: Categories"]) @@ -59,6 +59,7 @@ class RecipeCategoryController(BaseUserController): name=data.name, url=urls.category_url(data.slug, self.deps.settings.BASE_URL), ), + event_source=EventSource(event_type="create", item_type="category", item_id=data.id, slug=data.slug), ) return data @@ -84,6 +85,7 @@ class RecipeCategoryController(BaseUserController): name=data.name, url=urls.category_url(data.slug, self.deps.settings.BASE_URL), ), + event_source=EventSource(event_type="update", item_type="category", item_id=data.id, slug=data.slug), ) return data @@ -99,6 +101,7 @@ class RecipeCategoryController(BaseUserController): self.deps.acting_user.group_id, EventTypes.category_deleted, msg=self.t("notifications.generic-deleted", name=data.name), + event_source=EventSource(event_type="delete", item_type="category", item_id=data.id, slug=data.slug), ) # ========================================================================= diff --git a/mealie/routes/organizers/controller_tags.py b/mealie/routes/organizers/controller_tags.py index 24d6447b1c29..6fffbc6069dc 100644 --- a/mealie/routes/organizers/controller_tags.py +++ b/mealie/routes/organizers/controller_tags.py @@ -10,7 +10,7 @@ from mealie.schema.recipe import RecipeTagResponse, TagIn from mealie.schema.recipe.recipe import RecipeTag from mealie.schema.recipe.recipe_category import TagSave from mealie.services import urls -from mealie.services.event_bus_service.event_bus_service import EventBusService +from mealie.services.event_bus_service.event_bus_service import EventBusService, EventSource from mealie.services.event_bus_service.message_types import EventTypes router = APIRouter(prefix="/tags", tags=["Organizer: Tags"]) @@ -58,6 +58,7 @@ class TagController(BaseUserController): name=data.name, url=urls.tag_url(data.slug, self.deps.settings.BASE_URL), ), + event_source=EventSource(event_type="create", item_type="tag", item_id=data.id, slug=data.slug), ) return data @@ -75,6 +76,7 @@ class TagController(BaseUserController): name=data.name, url=urls.tag_url(data.slug, self.deps.settings.BASE_URL), ), + event_source=EventSource(event_type="update", item_type="tag", item_id=data.id, slug=data.slug), ) return data @@ -94,6 +96,7 @@ class TagController(BaseUserController): self.deps.acting_user.group_id, EventTypes.tag_deleted, msg=self.t("notifications.generic-deleted", name=data.name), + event_source=EventSource(event_type="delete", item_type="tag", item_id=data.id, slug=data.slug), ) @router.get("/slug/{tag_slug}", response_model=RecipeTagResponse) diff --git a/mealie/routes/recipe/recipe_crud_routes.py b/mealie/routes/recipe/recipe_crud_routes.py index 3cec9cabf53d..0613637ae264 100644 --- a/mealie/routes/recipe/recipe_crud_routes.py +++ b/mealie/routes/recipe/recipe_crud_routes.py @@ -28,7 +28,7 @@ from mealie.schema.recipe.recipe_scraper import ScrapeRecipeTest from mealie.schema.recipe.request_helpers import RecipeZipTokenResponse, UpdateImageResponse from mealie.schema.response.responses import ErrorResponse from mealie.services import urls -from mealie.services.event_bus_service.event_bus_service import EventBusService +from mealie.services.event_bus_service.event_bus_service import EventBusService, EventSource from mealie.services.event_bus_service.message_types import EventTypes from mealie.services.recipe.recipe_data_service import RecipeDataService from mealie.services.recipe.recipe_service import RecipeService @@ -162,6 +162,9 @@ class RecipeController(BaseRecipeController): name=new_recipe.name, url=urls.recipe_url(new_recipe.slug, self.deps.settings.BASE_URL), ), + event_source=EventSource( + event_type="create", item_type="recipe", item_id=new_recipe.id, slug=new_recipe.slug + ), ) return new_recipe.slug @@ -227,11 +230,27 @@ class RecipeController(BaseRecipeController): def create_one(self, data: CreateRecipe) -> str | None: """Takes in a JSON string and loads data into the database as a new entry""" try: - return self.service.create_one(data).slug + new_recipe = self.service.create_one(data) except Exception as e: self.handle_exceptions(e) return None + if new_recipe: + self.event_bus.dispatch( + self.deps.acting_user.group_id, + EventTypes.recipe_created, + msg=self.t( + "notifications.generic-created-with-url", + name=new_recipe.name, + url=urls.recipe_url(new_recipe.slug, self.deps.settings.BASE_URL), + ), + event_source=EventSource( + event_type="create", item_type="recipe", item_id=new_recipe.id, slug=new_recipe.slug + ), + ) + + return new_recipe.slug + @router.put("/{slug}") def update_one(self, slug: str, data: Recipe): """Updates a recipe by existing slug and data.""" @@ -249,6 +268,7 @@ class RecipeController(BaseRecipeController): name=data.name, url=urls.recipe_url(data.slug, self.deps.settings.BASE_URL), ), + event_source=EventSource(event_type="update", item_type="recipe", item_id=data.id, slug=data.slug), ) return data @@ -260,6 +280,19 @@ class RecipeController(BaseRecipeController): data = self.service.patch_one(slug, data) except Exception as e: self.handle_exceptions(e) + + if data: + self.event_bus.dispatch( + self.deps.acting_user.group_id, + EventTypes.recipe_updated, + msg=self.t( + "notifications.generic-updated-with-url", + name=data.name, + url=urls.recipe_url(data.slug, self.deps.settings.BASE_URL), + ), + event_source=EventSource(event_type="update", item_type="recipe", item_id=data.id, slug=data.slug), + ) + return data @router.delete("/{slug}") @@ -275,6 +308,7 @@ class RecipeController(BaseRecipeController): self.deps.acting_user.group_id, EventTypes.recipe_deleted, msg=self.t("notifications.generic-deleted", name=data.name), + event_source=EventSource(event_type="delete", item_type="recipe", item_id=data.id, slug=data.slug), ) return data diff --git a/mealie/services/event_bus_service/event_bus_service.py b/mealie/services/event_bus_service/event_bus_service.py index f3afd05494c7..a483a197845d 100644 --- a/mealie/services/event_bus_service/event_bus_service.py +++ b/mealie/services/event_bus_service/event_bus_service.py @@ -1,3 +1,5 @@ +from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit + from fastapi import BackgroundTasks, Depends from pydantic import UUID4 @@ -9,6 +11,27 @@ from .message_types import EventBusMessage, EventTypes from .publisher import ApprisePublisher, PublisherLike +class EventSource: + event_type: str + item_type: str + item_id: UUID4 | int + kwargs: dict + + def __init__(self, event_type: str, item_type: str, item_id: UUID4 | int, **kwargs) -> None: + self.event_type = event_type + self.item_type = item_type + self.item_id = item_id + self.kwargs = kwargs + + def dict(self) -> dict: + return { + "event_type": self.event_type, + "item_type": self.item_type, + "item_id": str(self.item_id), + **self.kwargs, + } + + class EventBusService: def __init__(self, bg: BackgroundTasks, session=Depends(generate_session)) -> None: self.bg = bg @@ -23,20 +46,26 @@ class EventBusService: def get_urls(self, event_type: EventTypes) -> list[str]: repos = AllRepositories(self.session) - notifiers: list[GroupEventNotifierPrivate] = repos.group_event_notifier.by_group(self.group_id).multi_query( - {"enabled": True}, override_schema=GroupEventNotifierPrivate - ) + notifiers: list[GroupEventNotifierPrivate] = repos.group_event_notifier.by_group( # type: ignore + self.group_id + ).multi_query({"enabled": True}, override_schema=GroupEventNotifierPrivate) return [notifier.apprise_url for notifier in notifiers if getattr(notifier.options, event_type.name)] - def dispatch(self, group_id: UUID4, event_type: EventTypes, msg: str = "") -> None: - self.group_id = group_id + def dispatch( + self, group_id: UUID4, event_type: EventTypes, msg: str = "", event_source: EventSource = None + ) -> None: + self.group_id = group_id # type: ignore - def _dispatch(): + def _dispatch(event_source: EventSource = None): if urls := self.get_urls(event_type): + if event_source: + urls = EventBusService.update_urls_with_event_source(urls, event_source) + self.publisher.publish(EventBusMessage.from_type(event_type, body=msg), urls) - self.bg.add_task(_dispatch) + if dispatch_task := _dispatch(event_source=event_source): + self.bg.add_task(dispatch_task) def test_publisher(self, url: str) -> None: self.bg.add_task( @@ -44,3 +73,28 @@ class EventBusService: event=EventBusMessage.from_type(EventTypes.test_message, body="This is a test event."), notification_urls=[url], ) + + @staticmethod + def update_urls_with_event_source(urls: list[str], event_source: EventSource): + return [ + # We use query params to add custom key: value pairs to the Apprise payload by prepending the key with ":". + EventBusService.merge_query_parameters(url, {f":{k}": v for k, v in event_source.dict().items()}) + # only certain endpoints support the custom key: value pairs, so we only apply them to those endpoints + if EventBusService.is_custom_url(url) else url + for url in urls + ] + + @staticmethod + def merge_query_parameters(url: str, params: dict): + scheme, netloc, path, query_string, fragment = urlsplit(url) + + # merge query params + query_params = parse_qs(query_string) + query_params.update(params) + new_query_string = urlencode(query_params, doseq=True) + + return urlunsplit((scheme, netloc, path, new_query_string, fragment)) + + @staticmethod + def is_custom_url(url: str): + return url.split(":", 1)[0].lower() in ["form", "forms", "json", "jsons", "xml", "xmls"] diff --git a/tests/integration_tests/user_group_tests/test_group_notifications.py b/tests/integration_tests/user_group_tests/test_group_notifications.py index abc4bfe65330..1b0f9726f4b9 100644 --- a/tests/integration_tests/user_group_tests/test_group_notifications.py +++ b/tests/integration_tests/user_group_tests/test_group_notifications.py @@ -1,14 +1,16 @@ from fastapi.testclient import TestClient from mealie.schema.group.group_events import GroupEventNotifierCreate, GroupEventNotifierOptions +from mealie.services.event_bus_service.event_bus_service import EventBusService, EventSource from tests.utils.assertion_helpers import assert_ignore_keys -from tests.utils.factories import random_bool, random_string +from tests.utils.factories import random_bool, random_email, random_int, random_string from tests.utils.fixture_schemas import TestUser class Routes: base = "/api/groups/events/notifications" + @staticmethod def item(item_id: int) -> str: return f"{Routes.base}/{item_id}" @@ -45,6 +47,10 @@ def notifier_generator(): ).dict(by_alias=True) +def event_source_generator(): + return EventSource(event_type=random_string, item_type=random_string(), item_id=random_int()) + + def test_create_notification(api_client: TestClient, unique_user: TestUser): payload = notifier_generator() response = api_client.post(Routes.base, json=payload, headers=unique_user.token) @@ -116,3 +122,37 @@ def test_delete_notification(api_client: TestClient, unique_user: TestUser): response = api_client.get(Routes.item(payload_as_dict["id"]), headers=unique_user.token) assert response.status_code == 404 + + +def test_event_bus_functions(): + test_event_source = event_source_generator() + + test_standard_urls = [ + "a" + random_string(), + f"ses://{random_email()}/{random_string()}/{random_string()}/us-east-1/", + f"pBUL://{random_string()}/{random_email()}", + ] + + test_custom_urls = [ + "JSON://" + random_string(), + f"jsons://{random_string()}:my/pass/word@{random_string()}.com/{random_string()}", + "form://" + random_string(), + "fORMS://" + str(random_int()), + "xml:" + str(random_int()), + "xmls://" + random_string(), + ] + + # Validate all standard urls are not considered custom + responses = [EventBusService.is_custom_url(url) for url in test_standard_urls] + assert not any(responses) + + # Validate all custom urls are actually considered custom + responses = [EventBusService.is_custom_url(url) for url in test_custom_urls] + assert all(responses) + + updated_standard_urls = EventBusService.update_urls_with_event_source(test_standard_urls, test_event_source) + updated_custom_urls = EventBusService.update_urls_with_event_source(test_custom_urls, test_event_source) + + # Validate that no URLs are lost when updating them + assert len(updated_standard_urls) == len(test_standard_urls) + assert len(updated_custom_urls) == len(updated_custom_urls) diff --git a/tests/utils/factories.py b/tests/utils/factories.py index d2c4e1975676..57d613b3e2e0 100644 --- a/tests/utils/factories.py +++ b/tests/utils/factories.py @@ -16,6 +16,10 @@ def random_bool() -> bool: return bool(random.getrandbits(1)) +def random_int(min=-4294967296, max=4294967296) -> int: + return random.randint(min, max) + + def user_registration_factory(advanced=None, private=None) -> CreateUserRegistration: return CreateUserRegistration( group=random_string(),