Skip to content

Commit

Permalink
Add support for notification type LIST_ACTIVITY_STREAM_REACTION
Browse files Browse the repository at this point in the history
  • Loading branch information
tr4nt0r committed Jan 21, 2025
1 parent c3239f5 commit 9175751
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 49 deletions.
4 changes: 4 additions & 0 deletions bring_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
BringUserUnknownException,
)
from .types import (
ActivityType,
BringActivityResponse,
BringAuthResponse,
BringAuthTokenResponse,
Expand All @@ -27,9 +28,11 @@
BringUserListSettingEntry,
BringUserSettingsEntry,
BringUserSettingsResponse,
ReactionType,
)

__all__ = [
"ActivityType",
"Bring",
"BringActivityResponse",
"BringAuthException",
Expand All @@ -52,4 +55,5 @@
"BringUserSettingsEntry",
"BringUserSettingsResponse",
"BringUserUnknownException",
"ReactionType",
]
63 changes: 51 additions & 12 deletions bring_api/bring.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
BringUserUnknownException,
)
from .types import (
Activity,
ActivityReaction,
ActivityType,
BringActivityResponse,
BringAuthResponse,
BringAuthTokenResponse,
Expand All @@ -45,6 +48,7 @@
BringNotificationType,
BringSyncCurrentUserResponse,
BringUserSettingsResponse,
ReactionType,
UserLocale,
)

Expand Down Expand Up @@ -646,22 +650,36 @@ async def notify(
list_uuid: str,
notification_type: BringNotificationType,
item_name: str | None = None,
activity: str | Activity | None = None,
receiver: str | None = None,
activity_type: ActivityType | None = None,
reaction: ReactionType | None = None,
) -> aiohttp.ClientResponse:
"""Send a push notification to all other members of a shared list.
Parameters
----------
list_uuid : str
A list uuid returned by loadLists()
The unique identifier of the list.
notification_type : BringNotificationType
The type of notification to be sent
The type of notification to be sent.
item_name : str, optional
The item_name **must** be included when notication_type
is BringNotificationType.URGENT_MESSAGE
The name of the item. Required if notification_type is URGENT_MESSAGE.
activity: str or Activity, optional
The UUID or the Activity object of the activity to react to.
Required if notification_type is LIST_ACTIVITY_STREAM_REACTION.
receiver: str, optional
The public user UUID of the recipient.
Required if notification_type is LIST_ACTIVITY_STREAM_REACTION and activity is referenced by it's uuid.
activity_type: ActivityType, optional
Required if notification_type is LIST_ACTIVITY_STREAM_REACTION and activity is referenced by it's uuid.
reaction: ReactionType, optional
The type of reaction. Either :MONOCLE:, :THUMBS_UP:, :HEART:, or :DROOLING:
Required if notification_type is LIST_ACTIVITY_STREAM_REACTION.
Returns
-------
Response
aiohttp.ClientResponse
The server response object.
Raises
Expand All @@ -671,12 +689,12 @@ async def notify(
BringAuthException
If the request fails due to invalid or expired authorization token.
TypeError
if the notification_type parameter is invalid.
If the notification_type parameter is invalid.
ValueError
If the value for item_name is invalid.
If the value for item_name, receiver, activity, activity_type, or reaction is invalid.
"""
json_data = BringNotificationsConfigType(
json = BringNotificationsConfigType(
arguments=[],
listNotificationType=notification_type.value,
senderPublicUserUuid=self.public_uuid,
Expand All @@ -687,18 +705,39 @@ async def notify(
f"notificationType {notification_type} not supported,"
"must be of type BringNotificationType."
)

if notification_type is BringNotificationType.URGENT_MESSAGE:
if not item_name or len(item_name) == 0:
raise ValueError(
"notificationType is URGENT_MESSAGE but argument itemName missing."
)
else:
json["arguments"] = [item_name]

if notification_type is BringNotificationType.LIST_ACTIVITY_STREAM_REACTION:
if isinstance(activity, Activity) and reaction:
json["receiverPublicUserUuid"] = activity.content.publicUserUuid
json["listActivityStreamReaction"] = ActivityReaction(
moduleUuid=activity.content.uuid,
moduleType=activity.type.name,
reactionType=reaction.name,
)
elif isinstance(activity, str) and receiver and activity_type and reaction:
json["receiverPublicUserUuid"] = receiver
json["listActivityStreamReaction"] = ActivityReaction(
moduleUuid=activity,
moduleType=activity_type.name,
reactionType=reaction.name,
)
else:
raise ValueError(
"notificationType is LIST_ACTIVITY_STREAM_REACTION but a parameter is missing. "
f"[{receiver=},{activity=},{activity_type=},{reaction=}]"
)

json_data["arguments"] = [item_name]
try:
url = self.url / "v2/bringnotifications/lists" / list_uuid
async with self._session.post(
url, headers=self.headers, json=json_data
) as r:
async with self._session.post(url, headers=self.headers, json=json) as r:
_LOGGER.debug(
"Response from %s [%s]: %s", url, r.status, await r.text()
)
Expand Down
87 changes: 54 additions & 33 deletions bring_api/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,55 @@

from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum, StrEnum
from enum import StrEnum
from typing import Literal, NotRequired, TypedDict

from mashumaro.mixins.orjson import DataClassORJSONMixin


class ActivityType(StrEnum):
"""Activity type."""

LIST_ITEMS_CHANGED = "LIST_ITEMS_CHANGED"
LIST_ITEMS_ADDED = "LIST_ITEMS_ADDED"
LIST_ITEMS_REMOVED = "LIST_ITEMS_REMOVED"


class BringNotificationType(StrEnum):
"""Notification type.
GOING_SHOPPING: "I'm going shopping! - Last chance for adjustments"
CHANGED_LIST: "List changed - Check it out"
SHOPPING_DONE: "Shopping done - you can relax"
URGENT_MESSAGE: "Breaking news - Please get {itemName}!
LIST_ACTIVITY_STREAM_REACTION: React with 🧐, 👍🏼, 🤤 or 💜 to activity
"""

GOING_SHOPPING = "GOING_SHOPPING"
CHANGED_LIST = "CHANGED_LIST"
SHOPPING_DONE = "SHOPPING_DONE"
URGENT_MESSAGE = "URGENT_MESSAGE"
LIST_ACTIVITY_STREAM_REACTION = "LIST_ACTIVITY_STREAM_REACTION"


class ReactionType(StrEnum):
"""Activity reaction types."""

THUMBS_UP = "THUMBS_UP"
MONOCLE = "MONOCLE"
DROOLING = "DROOLING"
HEART = "HEART"


class BringItemOperation(StrEnum):
"""Operation to be be executed on list items."""

ADD = "TO_PURCHASE"
COMPLETE = "TO_RECENTLY"
REMOVE = "REMOVE"
ATTRIBUTE_UPDATE = "ATTRIBUTE_UPDATE"


@dataclass(kw_only=True)
class BringList(DataClassORJSONMixin):
"""A list class. Represents a single list."""
Expand Down Expand Up @@ -108,27 +151,22 @@ class BringListItemsDetailsResponse(DataClassORJSONMixin):
items: list[BringListItemDetails]


class BringNotificationType(Enum):
"""Notification type.
GOING_SHOPPING: "I'm going shopping! - Last chance for adjustments"
CHANGED_LIST: "List changed - Check it out"
SHOPPING_DONE: "Shopping done - you can relax"
URGENT_MESSAGE: "Breaking news - Please get {itemName}!
"""
class ActivityReaction(TypedDict):
"""Reaction config."""

GOING_SHOPPING = "GOING_SHOPPING"
CHANGED_LIST = "CHANGED_LIST"
SHOPPING_DONE = "SHOPPING_DONE"
URGENT_MESSAGE = "URGENT_MESSAGE"
moduleUuid: str
moduleType: str
reactionType: str


class BringNotificationsConfigType(TypedDict):
class BringNotificationsConfigType(TypedDict, total=True):
"""A notification config."""

arguments: list[str]
arguments: NotRequired[list[str]]
listNotificationType: str
senderPublicUserUuid: str
receiverPublicUserUuid: NotRequired[str]
listActivityStreamReaction: NotRequired[ActivityReaction]


@dataclass(kw_only=True)
Expand Down Expand Up @@ -189,15 +227,6 @@ class BringSyncCurrentUserResponse(DataClassORJSONMixin):
photoPath: str = ""


class BringItemOperation(StrEnum):
"""Operation to be be executed on list items."""

ADD = "TO_PURCHASE"
COMPLETE = "TO_RECENTLY"
REMOVE = "REMOVE"
ATTRIBUTE_UPDATE = "ATTRIBUTE_UPDATE"


class BringAttribute(TypedDict):
"""An attribute dict. Represents a single item attribute."""

Expand Down Expand Up @@ -233,14 +262,6 @@ class BringAuthTokenResponse(DataClassORJSONMixin):
expires_in: int


class ActivityType(StrEnum):
"""Activity type."""

LIST_ITEMS_CHANGED = "LIST_ITEMS_CHANGED"
LIST_ITEMS_ADDED = "LIST_ITEMS_ADDED"
LIST_ITEMS_REMOVED = "LIST_ITEMS_REMOVED"


@dataclass
class ActivityContent:
"""An activity content entry."""
Expand All @@ -254,7 +275,7 @@ class ActivityContent:


@dataclass
class Activity:
class Activity(DataClassORJSONMixin):
"""An activity entry."""

type: ActivityType
Expand Down
53 changes: 49 additions & 4 deletions tests/test_bring.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import time

import aiohttp
from aioresponses import aioresponses
from dotenv import load_dotenv
import pytest
from syrupy.assertion import SnapshotAssertion
Expand All @@ -22,11 +23,13 @@
)
from bring_api.helpers import headers_deserialize, headers_serialize
from bring_api.types import (
ActivityType,
BringItem,
BringItemOperation,
BringNotificationType,
BringSyncCurrentUserResponse,
BringUserSettingsResponse,
ReactionType,
UserLocale,
)

Expand Down Expand Up @@ -304,17 +307,17 @@ class TestNotifications:
@pytest.mark.parametrize(
("notification_type", "item_name"),
[
(BringNotificationType.GOING_SHOPPING, ""),
(BringNotificationType.CHANGED_LIST, ""),
(BringNotificationType.SHOPPING_DONE, ""),
(BringNotificationType.GOING_SHOPPING, None),
(BringNotificationType.CHANGED_LIST, None),
(BringNotificationType.SHOPPING_DONE, None),
(BringNotificationType.URGENT_MESSAGE, "WITH_ITEM_NAME"),
],
)
async def test_notify(
self,
bring,
notification_type: BringNotificationType,
item_name: str,
item_name: str | None,
mocked,
):
"""Test GOING_SHOPPING notification."""
Expand All @@ -326,6 +329,48 @@ async def test_notify(
resp = await bring.notify(UUID, notification_type, item_name)
assert resp.status == HTTPStatus.OK

@pytest.mark.parametrize(
"activity_type",
[
ActivityType.LIST_ITEMS_REMOVED,
ActivityType.LIST_ITEMS_ADDED,
ActivityType.LIST_ITEMS_CHANGED,
],
)
@pytest.mark.parametrize(
"reaction",
[
ReactionType.THUMBS_UP,
ReactionType.MONOCLE,
ReactionType.DROOLING,
ReactionType.HEART,
],
)
async def test_notify_activity_stream_reaction(
self,
bring: Bring,
mocked: aioresponses,
activity_type: ActivityType,
reaction: ReactionType,
):
"""Test GOING_SHOPPING notification."""

mocked.post(
f"https://api.getbring.com/rest/v2/bringnotifications/lists/{UUID}",
status=HTTPStatus.OK,
)

resp = await bring.notify(
UUID,
BringNotificationType.LIST_ACTIVITY_STREAM_REACTION,
receiver=UUID,
activity=UUID,
activity_type=activity_type,
reaction=reaction,
)
assert resp.status == HTTPStatus.OK
mocked.assert_called_once()

async def test_notify_urgent_message_item_name_missing(self, bring, mocked):
"""Test URGENT_MESSAGE notification."""
mocked.post(
Expand Down

0 comments on commit 9175751

Please sign in to comment.