mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-24 01:12:54 -04:00
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)
This commit is contained in:
parent
3030e3e7f4
commit
754e77c9cb
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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} видалено"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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"]
|
||||
|
@ -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)
|
||||
|
@ -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(),
|
||||
|
Loading…
x
Reference in New Issue
Block a user