Feature/group based notifications (#918)

* fix group page

* setup group notification for backend

* update type generators

* script to auto-generate schema exports

* setup frontend CRUD interface

* remove old notifications UI

* drop old events api

* add test functionality

* update naming for fields

* add event dispatcher functionality

* bump to python 3.10

* bump python version

* purge old event code

* use-async apprise

* set mealie logo as image

* unify styles for buttons rows

* add links to banners
This commit is contained in:
Hayden
2022-01-09 21:04:24 -09:00
committed by GitHub
parent 50a341ed3f
commit 190773c5d7
74 changed files with 1992 additions and 1229 deletions

View File

@@ -9,8 +9,7 @@ from sqlalchemy.orm.session import Session
from mealie.core.config import get_app_dirs
from mealie.repos.all_repositories import get_repositories
from mealie.schema.admin import CommentImport, GroupImport, NotificationImport, RecipeImport, UserImport
from mealie.schema.events import EventNotificationIn
from mealie.schema.admin import CommentImport, GroupImport, RecipeImport, UserImport
from mealie.schema.recipe import Recipe, RecipeCommentOut
from mealie.schema.user import PrivateUser, UpdateGroup
from mealie.services.image import minify
@@ -159,24 +158,6 @@ class ImportDatabase:
minify.migrate_images()
def import_notifications(self):
notify_file = self.import_dir.joinpath("notifications", "notifications.json")
notifications = ImportDatabase.read_models_file(notify_file, EventNotificationIn)
import_notifications = []
for notify in notifications:
import_status = self.import_model(
db_table=self.db.event_notifications,
model=notify,
return_model=NotificationImport,
name_attr="name",
search_key="notification_url",
)
import_notifications.append(import_status)
return import_notifications
def import_settings(self):
return []
@@ -330,11 +311,6 @@ def import_database(
user_report = import_session.import_users()
notification_report = []
if import_notifications:
notification_report = import_session.import_notifications()
# if import_recipes:
# import_session.import_comments()
import_session.clean_up()

View File

@@ -0,0 +1,46 @@
from fastapi import BackgroundTasks, Depends
from pydantic import UUID4
from mealie.db.db_setup import generate_session
from mealie.repos.repository_factory import AllRepositories
from mealie.schema.group.group_events import GroupEventNotifierPrivate
from .message_types import EventBusMessage, EventTypes
from .publisher import ApprisePublisher, PublisherLike
class EventBusService:
def __init__(self, bg: BackgroundTasks, session=Depends(generate_session)) -> None:
self.bg = bg
self._publisher = ApprisePublisher
self.session = session
self.group_id = None
@property
def publisher(self) -> PublisherLike:
return self._publisher()
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
)
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():
if urls := self.get_urls(event_type):
self.publisher.publish(EventBusMessage.from_type(event_type, body=msg), urls)
self.bg.add_task(_dispatch)
def test_publisher(self, url: str) -> None:
self.bg.add_task(
self.publisher.publish,
event=EventBusMessage.from_type(EventTypes.test_message, body="This is a test event."),
notification_urls=[url],
)

View File

@@ -0,0 +1,47 @@
from enum import Enum, auto
class EventTypes(Enum):
test_message = auto()
recipe_created = auto()
recipe_updated = auto()
recipe_deleted = auto()
user_signup = auto()
data_migrations = auto()
data_export = auto()
data_import = auto()
mealplan_entry_created = auto()
shopping_list_created = auto()
shopping_list_updated = auto()
shopping_list_deleted = auto()
cookbook_created = auto()
cookbook_updated = auto()
cookbook_deleted = auto()
tag_created = auto()
tag_updated = auto()
tag_deleted = auto()
category_created = auto()
category_updated = auto()
category_deleted = auto()
class EventBusMessage:
title: str
body: str = ""
def __init__(self, title, body) -> None:
self.title = title
self.body = body
@classmethod
def from_type(cls, event_type: EventTypes, body: str = "") -> "EventBusMessage":
title = event_type.name.replace("_", " ").title()
return cls(title=title, body=body)

View File

@@ -0,0 +1,29 @@
from typing import Protocol
import apprise
from mealie.services.event_bus_service.event_bus_service import EventBusMessage
class PublisherLike(Protocol):
def publish(self, event: EventBusMessage, notification_urls: list[str]):
...
class ApprisePublisher:
def __init__(self, hard_fail=False) -> None:
asset = apprise.AppriseAsset(
async_mode=True,
image_url_mask="https://raw.githubusercontent.com/hay-kot/mealie/dev/frontend/public/img/icons/android-chrome-maskable-512x512.png",
)
self.apprise = apprise.Apprise(asset=asset)
self.hard_fail = hard_fail
def publish(self, event: EventBusMessage, notification_urls: list[str]):
for dest in notification_urls:
status = self.apprise.add(dest)
if not status and self.hard_fail:
raise Exception("Apprise URL Add Failed")
self.apprise.notify(title=event.title, body=event.body)

View File

@@ -1,4 +1,3 @@
import apprise
from sqlalchemy.orm.session import Session
from mealie.db.db_setup import create_session
@@ -6,44 +5,12 @@ from mealie.repos.all_repositories import get_repositories
from mealie.schema.events import Event, EventCategory
def test_notification(notification_url, event=None) -> bool:
if event is None:
event = Event(
title="Test Notification",
text="This is a test message from the Mealie API server",
category=EventCategory.general.value,
)
post_notifications(event, [notification_url], hard_fail=True)
def post_notifications(event: Event, notification_urls=list[str], hard_fail=False, attachment=None):
asset = apprise.AppriseAsset(async_mode=False)
apobj = apprise.Apprise(asset=asset)
for dest in notification_urls:
status = apobj.add(dest)
if not status and hard_fail:
raise Exception("Apprise URL Add Failed")
apobj.notify(
body=event.text,
title=event.title,
attach=str(attachment),
)
def save_event(title, text, category, session: Session, attachment=None):
def save_event(title, text, category, session: Session):
event = Event(title=title, text=text, category=category)
session = session or create_session()
db = get_repositories(session)
db.events.create(event.dict())
notification_objects = db.event_notifications.get(match_value=True, match_key=category, limit=9999)
notification_urls = [x.notification_url for x in notification_objects]
post_notifications(event, notification_urls, attachment=attachment)
def create_general_event(title, text, session=None):
category = EventCategory.general
@@ -52,8 +19,7 @@ def create_general_event(title, text, session=None):
def create_recipe_event(title, text, session=None, attachment=None):
category = EventCategory.recipe
save_event(title=title, text=text, category=category, session=session, attachment=attachment)
save_event(title=title, text=text, category=category, session=session)
def create_backup_event(title, text, session=None):