Skip to content

Commit 1b77d27

Browse files
committed
Add payloads
1 parent cbf813e commit 1b77d27

6 files changed

Lines changed: 203 additions & 14 deletions

File tree

hoard/campaigns/consumers.py

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,15 @@
4141
InvitationEvent,
4242
MembershipEvent,
4343
)
44-
from .payloads import CampaignCalendarChangedEvent, CampaignCalendarData
44+
from .payloads import (
45+
CampaignCalendarChangedEvent,
46+
CampaignCalendarData,
47+
CampaignInvitationChangedEvent,
48+
CampaignInvitationData,
49+
CampaignLevelChangedEvent,
50+
CampaignMemberData,
51+
CampaignMembershipChangedEvent,
52+
)
4553
from .protocol import (
4654
CommandAcknowledgementEnvelope,
4755
QueryResultEnvelope,
@@ -615,7 +623,12 @@ async def _request_response(self, content: dict[str, object], handler) -> None:
615623
request_id=envelope.request_id,
616624
data=result_data,
617625
).model_dump(mode="json")
618-
elif message_type == "campaign.calendar.adjust":
626+
elif message_type in {
627+
"campaign.calendar.adjust",
628+
"campaign.members.deactivate",
629+
"campaign.invites.revoke",
630+
"campaign.level.approve",
631+
}:
619632
response = CommandAcknowledgementEnvelope(
620633
request_id=envelope.request_id
621634
).model_dump(mode="json")
@@ -770,7 +783,18 @@ def _member_deactivate(self, content: dict[str, object]) -> None:
770783
character.is_archived = True
771784
character.archived_at = timezone.now()
772785
character.save(update_fields=("is_active", "is_archived", "archived_at"))
773-
notify_campaign_changed(context.campaign_id)
786+
notify_campaign_event(
787+
context.campaign_id,
788+
CampaignMembershipChangedEvent(
789+
member=CampaignMemberData(
790+
id=candidate.pk,
791+
username=candidate.user.get_username(),
792+
is_game_master=candidate.kind == CampaignContext.Kind.GM,
793+
is_active=candidate.is_active,
794+
),
795+
request_id=str(content["request_id"]),
796+
),
797+
)
774798

775799
@database_sync_to_async
776800
def _invite_list(self, content: dict[str, object]) -> list[dict[str, object]]:
@@ -798,6 +822,15 @@ def _invite_create(self, content: dict[str, object]) -> dict[str, object]:
798822
None,
799823
[email],
800824
)
825+
notify_campaign_event(
826+
context.campaign_id,
827+
CampaignInvitationChangedEvent(
828+
invitation=CampaignInvitationData.model_validate(
829+
self._invitation_data(invitation)
830+
),
831+
request_id=str(content["request_id"]),
832+
),
833+
)
801834
return {**self._invitation_data(invitation), "link": link}
802835

803836
@database_sync_to_async
@@ -832,6 +865,15 @@ def _invite_resend(self, content: dict[str, object]) -> dict[str, object]:
832865
None,
833866
[invitation.delivery_email],
834867
)
868+
notify_campaign_event(
869+
context.campaign_id,
870+
CampaignInvitationChangedEvent(
871+
invitation=CampaignInvitationData.model_validate(
872+
self._invitation_data(invitation)
873+
),
874+
request_id=str(content["request_id"]),
875+
),
876+
)
835877
return {**self._invitation_data(invitation), "link": link}
836878

837879
@database_sync_to_async
@@ -855,6 +897,15 @@ def _invite_revoke(self, content: dict[str, object]) -> None:
855897
created_by=context,
856898
reason=InvitationEvent.Reason.REVOKED,
857899
)
900+
notify_campaign_event(
901+
context.campaign_id,
902+
CampaignInvitationChangedEvent(
903+
invitation=CampaignInvitationData.model_validate(
904+
self._invitation_data(invitation)
905+
),
906+
request_id=str(content["request_id"]),
907+
),
908+
)
858909

859910
@database_sync_to_async
860911
def _level_status(self, content: dict[str, object]) -> dict[str, object]:
@@ -876,7 +927,14 @@ def _level_approve(self, content: dict[str, object]) -> dict[str, object]:
876927
context = self._context()
877928
_gm(context)
878929
event = approve_campaign_level(context.campaign, created_by=context)
879-
notify_campaign_changed(context.campaign_id)
930+
notify_campaign_event(
931+
context.campaign_id,
932+
CampaignLevelChangedEvent(
933+
previous_level=event.previous_level,
934+
next_level=event.next_level,
935+
request_id=str(content["request_id"]),
936+
),
937+
)
880938
return {
881939
"previous_level": event.previous_level,
882940
"next_level": event.next_level,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""Pydantic contracts shared by WebSocket and REST compatibility transports."""
2+
3+
from .calendar import (
4+
CalendarAdjustmentCommand,
5+
CampaignCalendarChangedEvent,
6+
CampaignCalendarData,
7+
)
8+
from .campaign import CampaignLevelChangedEvent, CampaignStateChangedEvent
9+
from .invitations import (
10+
CampaignInvitationChangedEvent,
11+
CampaignInvitationData,
12+
CampaignMemberData,
13+
CampaignMembershipChangedEvent,
14+
InvitationCreateCommand,
15+
InvitationIdentifierCommand,
16+
MemberDeactivationCommand,
17+
)
18+
19+
__all__ = [
20+
"CalendarAdjustmentCommand",
21+
"CampaignCalendarChangedEvent",
22+
"CampaignCalendarData",
23+
"CampaignInvitationChangedEvent",
24+
"CampaignInvitationData",
25+
"CampaignLevelChangedEvent",
26+
"CampaignMemberData",
27+
"CampaignMembershipChangedEvent",
28+
"CampaignStateChangedEvent",
29+
"InvitationCreateCommand",
30+
"InvitationIdentifierCommand",
31+
"MemberDeactivationCommand",
32+
]
Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
"""Pydantic contracts shared by WebSocket and REST compatibility transports."""
1+
"""Calendar query, command, and event contracts."""
22

33
from __future__ import annotations
44

55
from typing import Literal
66

77
from pydantic import BaseModel
88

9-
from .models import Campaign
9+
from ..models import Campaign
1010

1111

1212
class CampaignCalendarData(BaseModel):
@@ -19,6 +19,7 @@ class CampaignCalendarData(BaseModel):
1919

2020
@classmethod
2121
def from_campaign(cls, campaign: Campaign) -> CampaignCalendarData:
22+
"""Build the transport contract from the authoritative campaign model."""
2223
return cls(
2324
era_abbreviation=campaign.calendar_era_abbreviation,
2425
era_name=campaign.calendar_era_name,
@@ -33,13 +34,6 @@ class CalendarAdjustmentCommand(BaseModel):
3334
amount: Literal[-1, 1]
3435

3536

36-
class CampaignStateChangedEvent(BaseModel):
37-
"""Compatibility notification for a changed campaign render model."""
38-
39-
type: Literal["campaign.state_changed"] = "campaign.state_changed"
40-
request_id: str | None = None
41-
42-
4337
class CampaignCalendarChangedEvent(BaseModel):
4438
"""Authoritative calendar state published after a calendar command."""
4539

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Campaign-wide event contracts."""
2+
3+
from typing import Literal
4+
5+
from pydantic import BaseModel
6+
7+
8+
class CampaignLevelChangedEvent(BaseModel):
9+
"""Authoritative campaign level after a GM approves a level-up."""
10+
11+
type: Literal["campaign.level_changed"] = "campaign.level_changed"
12+
previous_level: int
13+
next_level: int
14+
request_id: str | None = None
15+
16+
17+
class CampaignStateChangedEvent(BaseModel):
18+
"""Compatibility notification for a changed campaign render model."""
19+
20+
type: Literal["campaign.state_changed"] = "campaign.state_changed"
21+
request_id: str | None = None
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""Campaign membership and invitation contracts."""
2+
3+
from typing import Literal
4+
5+
from pydantic import BaseModel, Field
6+
7+
8+
class MemberDeactivationCommand(BaseModel):
9+
"""Validated input for deactivating a campaign member."""
10+
11+
member_id: int = Field(gt=0)
12+
13+
14+
class InvitationCreateCommand(BaseModel):
15+
"""Validated input for creating a campaign invitation."""
16+
17+
email: str = Field(default="", max_length=254)
18+
19+
20+
class InvitationIdentifierCommand(BaseModel):
21+
"""Validated input for commands that address one invitation."""
22+
23+
invitation_id: int = Field(gt=0)
24+
25+
26+
class CampaignMemberData(BaseModel):
27+
"""The public membership fields required by campaign management clients."""
28+
29+
id: int
30+
username: str
31+
is_game_master: bool
32+
is_active: bool
33+
34+
35+
class CampaignInvitationData(BaseModel):
36+
"""The non-secret state of a campaign invitation."""
37+
38+
id: int
39+
email: str
40+
created_at: str
41+
expires_at: str
42+
accepted_at: str | None
43+
status: Literal["pending", "accepted", "expired", "revoked"]
44+
45+
46+
class CampaignMembershipChangedEvent(BaseModel):
47+
"""Authoritative member state after a membership command."""
48+
49+
type: Literal["campaign.membership_changed"] = "campaign.membership_changed"
50+
member: CampaignMemberData
51+
request_id: str | None = None
52+
53+
54+
class CampaignInvitationChangedEvent(BaseModel):
55+
"""Authoritative non-secret invitation state after an invitation command."""
56+
57+
type: Literal["campaign.invitation_changed"] = "campaign.invitation_changed"
58+
invitation: CampaignInvitationData
59+
request_id: str | None = None

hoard/campaigns/protocol.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,13 @@ class OperationDefinition:
181181

182182
def operation_definitions() -> dict[str, OperationDefinition]:
183183
"""Return the registered contracts for every currently supported operation."""
184-
from .payloads import CalendarAdjustmentCommand, CampaignCalendarData
184+
from .payloads import (
185+
CalendarAdjustmentCommand,
186+
CampaignCalendarData,
187+
InvitationCreateCommand,
188+
InvitationIdentifierCommand,
189+
MemberDeactivationCommand,
190+
)
185191

186192
definitions = {
187193
name: OperationDefinition(name=name, kind=OperationKind.QUERY)
@@ -205,6 +211,25 @@ def operation_definitions() -> dict[str, OperationDefinition]:
205211
payload_model=CalendarAdjustmentCommand,
206212
result_model=CampaignCalendarData,
207213
)
214+
definitions["campaign.members.deactivate"] = OperationDefinition(
215+
name="campaign.members.deactivate",
216+
kind=OperationKind.COMMAND,
217+
payload_model=MemberDeactivationCommand,
218+
)
219+
definitions["campaign.invites.create"] = OperationDefinition(
220+
name="campaign.invites.create",
221+
kind=OperationKind.COMMAND,
222+
payload_model=InvitationCreateCommand,
223+
)
224+
for name in (
225+
"campaign.invites.resend",
226+
"campaign.invites.revoke",
227+
):
228+
definitions[name] = OperationDefinition(
229+
name=name,
230+
kind=OperationKind.COMMAND,
231+
payload_model=InvitationIdentifierCommand,
232+
)
208233

209234
return definitions
210235

0 commit comments

Comments
 (0)