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:
Michael Genson 2022-06-15 14:49:42 -05:00 committed by GitHub
parent 3030e3e7f4
commit 754e77c9cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 296 additions and 54 deletions

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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} видалено"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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

View File

@ -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

View File

@ -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),
)
# =========================================================================

View File

@ -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)

View File

@ -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

View File

@ -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"]

View File

@ -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)

View File

@ -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(),