angee.messaging.schema
GraphQL schema for the messaging addon — threads, messages, and the inbox.
Messages arrive through channel sync and the manager-owned ingest path; the console browses and moderates them through Hasura resources. Parts, participants, edges, and reactions remain nested read projections reached through their message/thread owners.
ChannelType
@strawberry_django.type(Channel)
class ChannelType(IntegrationLabelMixin, BridgeSyncStatusMixin, AngeeNode)GraphQL projection of a connected message channel (e.g. an email account).
pairing_state
@strawberry_django.field
def pairing_state() -> PairingState | NoneReturn the live bridge's actual pairing state, or null for poll channels.
MessagingPairingQuery
@strawberry.type
class MessagingPairingQuery()Admin pairing state for live message channels.
channel_pairing
@strawberry.field(permission_classes=ADMIN_PERMISSION_CLASSES)
def channel_pairing(id: PublicID) -> PairingProjectionReturn pairing state in the caller's channel scope.
MessagingPairingMutation
@strawberry.type
class MessagingPairingMutation()Admin lifecycle verbs for live message-channel pairing.
resume_channel_pairing
@strawberry.mutation(permission_classes=ADMIN_PERMISSION_CLASSES)
def resume_channel_pairing(id: PublicID) -> ActionResultResume retained pairing material or start a new pairing session.
submit_channel_password
@strawberry.mutation(permission_classes=ADMIN_PERMISSION_CLASSES)
@sensitive_variables("password", "material")
def submit_channel_password(id: PublicID, password: str) -> ActionResultSubmit one consume-once account password to the live channel session.
skip_channel_password
@strawberry.mutation(permission_classes=ADMIN_PERMISSION_CLASSES)
def skip_channel_password(id: PublicID) -> ActionResultSkip one optional consume-once secret round.
reset_channel_pairing
@strawberry.mutation(permission_classes=ADMIN_PERMISSION_CLASSES)
def reset_channel_pairing(id: PublicID) -> ActionResultWipe released pairing material and restart with a fresh session.
disconnect_channel
@strawberry.mutation(permission_classes=ADMIN_PERMISSION_CLASSES)
def disconnect_channel(id: PublicID) -> ActionResultStop the live session while retaining reusable pairing material.
MessagingChannelMutation
@strawberry.type
class MessagingChannelMutation()Owner/admin console mutation for deleting a connected channel.
delete_channel
@strawberry.mutation(name="delete_channel")
def delete_channel(id: PublicID, confirm: bool = False) -> DeletePreviewPreview, then optionally purge, one channel and everything it ingested.
Deleting a channel is a purge: its threads and messages FK the shared integrate.Integration parent with SET_NULL, so removing the channel row alone would orphan them. The preview forecasts the channel + its Integration row
- the channel's thread/message totals and their CASCADE children as deleted — the single inventory :meth:
ChannelManager.inventorycounts with.count(), never materialized, so it stays fast on a 90k-message channel — andconfirmruns that same scope through :meth:ChannelManager.purge(:meth:ThreadManager.teardown_for_channelfirst, then the channel + Integration row). Gated by the channel's owndeletepermission (its integration owner or a platform admin): a caller who cannot write the channel never resolves it, and :meth:ChannelManager.purgere-checksdeletebefore any destructive work. APROTECT/RESTRICTblocker on the confirm delete is surfaced as a blocked preview rather than a raw error.
FragmentType
@strawberry_django.type(Fragment)
class FragmentType(AngeeNode)GraphQL projection of a content-addressed text fragment.
part_count
@strawberry.field(name="part_count")
def part_count() -> intHow many parts (across all messages) share this fragment — the dedup fact.
message_count
@strawberry.field(name="message_count")
def message_count() -> intHow many distinct messages reference this fragment through their parts.
PartType
@strawberry_django.type(Part)
class PartType(AngeeNode)GraphQL projection of one message body part.
MessageSubtypeType
@strawberry_django.type(MessageSubtype)
class MessageSubtypeType(AngeeNode)GraphQL projection of an Odoo-style message subtype.
TrackingValueType
@strawberry_django.type(TrackingValue)
class TrackingValueType(AngeeNode)GraphQL projection of one tracked field change on a chatter message.
ParticipantType
@strawberry_django.type(Participant)
class ParticipantType(AngeeNode)GraphQL projection of a thread/message participant.
MessageReactionGroupType
@strawberry.type
class MessageReactionGroupType()Odoo-style grouped reactions for one message.
RecordHandleType
@strawberry_django.type(Handle)
class RecordHandleType(AngeeNode)Envelope identity exposed by record chatter, without party backedges.
RecordMessageReactionGroupType
@strawberry.type
class RecordMessageReactionGroupType()A record-message reaction summary with narrowed handle projections.
MessageType
@strawberry_django.type(Message)
class MessageType(AngeeNode)GraphQL projection of a message.
parts
@strawberry.field
def parts() -> list[PartType]Return this message's MIME parts in depth-first reading order.
title
@strawberry.field
def title() -> strReturn the message's title text — its TITLE part's fragment, or "".
No optimizer prefetch hint here: a query that also selects parts makes the optimizer build its own Prefetch("parts", …), and Django rejects a second queryset for the same lookup. The model method rides an existing parts prefetch when one is loaded and otherwise costs one indexed probe.
edges
@strawberry.field
def edges() -> list["MessageEdgeType"]Return this message's cross-message edges, both directions.
The fragment-derived quote graph plus post relations — the "how it interconnects" read the content data view renders. Actor-scoped end to end: the owner (MessageEdgeManager.for_message) keeps only edges whose far endpoint the actor may read, because quote edges cross channels by construction.
reaction_groups
@strawberry.field
def reaction_groups(info: strawberry.Info) -> list[MessageReactionGroupType]Return reactions grouped by content, with current-user state.
starred
@strawberry.field
def starred(info: strawberry.Info) -> boolReturn whether the current user has starred this message.
needaction
@strawberry.field
def needaction(info: strawberry.Info) -> boolReturn whether this message sits past the current user's read receipt.
can_edit
@strawberry_django.field(prefetch_related=["tracking_values"])
def can_edit(info: strawberry.Info) -> boolReturn whether the current actor may edit this message's body.
Delegates to the message's own :meth:Message.can_edit owner (post access plus the mail edit rule), passing the record post access resolved and memoized once per thread by :func:_message_post_access. The tracking_values prefetch hint lets the optimizer batch the edit-rule predicate instead of an exists() per row.
can_delete
@strawberry.field
def can_delete(info: strawberry.Info) -> boolReturn whether the current actor may delete this message.
Delegates to :meth:Message.can_delete (the record thread's post access; deletion carries no mail-kind restriction of its own), passing the post access memoized once per thread by :func:_message_post_access.
RecordMessageParentType
@strawberry_django.type(Message)
class RecordMessageParentType(AngeeNode)Minimal parent-message context shown in record chatter.
RecordMessageType
@strawberry_django.type(Message)
class RecordMessageType(AngeeNode)Record-gated message projection without generic inbox relation backedges.
parts
@strawberry.field
def parts() -> list[PartType]Return this record message's MIME parts in depth-first reading order.
title
@strawberry.field
def title() -> strReturn the message title text from its sparse title part.
reaction_groups
@strawberry.field
def reaction_groups(
info: strawberry.Info) -> list[RecordMessageReactionGroupType]Return reaction groups with record-safe handle projections.
starred
@strawberry.field
def starred(info: strawberry.Info) -> boolReturn whether the current user has starred this message.
needaction
@strawberry.field
def needaction(info: strawberry.Info) -> boolReturn whether this message sits past the current user's read receipt.
can_edit
@strawberry_django.field(prefetch_related=["tracking_values"])
def can_edit(info: strawberry.Info) -> boolReturn the model-owned edit capability for this record message.
can_delete
@strawberry.field
def can_delete(info: strawberry.Info) -> boolReturn the model-owned delete capability for this record message.
ThreadType
@strawberry_django.type(Thread)
class ThreadType(AngeeNode)GraphQL projection of a thread.
RecordThreadType
@strawberry_django.type(Thread)
class RecordThreadType(AngeeNode)Record-gated thread summary without generic inbox backedges.
RecordThreadFollowerType
@strawberry_django.type(ThreadFollower)
class RecordThreadFollowerType(AngeeNode)Record-scoped follower projection without message or thread backedges.
RecordThreadNotificationType
@strawberry_django.type(ThreadNotification)
class RecordThreadNotificationType(AngeeNode)Record-scoped notification projection with narrowed message context.
RecordThreadActivityType
@strawberry_django.type(ThreadActivity)
class RecordThreadActivityType(AngeeNode)Record-scoped activity projection without thread or attachment backedges.
state
@strawberry.field
def state() -> strReturn the Odoo-style activity state.
ThreadAttachmentType
@strawberry_django.type(ThreadAttachment)
class ThreadAttachmentType(AngeeNode)GraphQL projection of a model-record thread attachment.
model_label
@strawberry.field(name="model_label")
def model_label() -> strReturn the attached target's model label (app_label.ModelName).
record_id
@strawberry.field(name="record_id")
def record_id() -> strawberry.IDReturn the attached target's stable public id (its sqid).
A parent pointer back to the record — the ID scalar every record reference uses (RecordReferenceInput.record_id); navigating it re-gates through the target's own record read (an assignee not granted the record may 404).
ThreadFollowerType
@strawberry_django.type(ThreadFollower)
class ThreadFollowerType(AngeeNode)GraphQL projection of a user's thread membership — policy plus read receipt.
ThreadActivityType
@strawberry_django.type(ThreadActivity)
class ThreadActivityType(AngeeNode)GraphQL projection of a scheduled record chatter activity.
state
@strawberry.field
def state() -> strReturn the Odoo-style activity state.
RecordPointerType
@strawberry.type
class RecordPointerType()Minimal, read-only pointer back to the record a chatter row is attached to.
The record's human label, its app_label.ModelName, and its stable public id — the three facts a cross-record agenda needs to link back, each computed from the owning ThreadAttachment row alone (no target load, no parent-record read). Navigating the link re-gates through the record's own read (an assignee not granted the record may 404).
AgendaActivityType
@strawberry_django.type(ThreadActivity)
class AgendaActivityType(AngeeNode)The actor's own scheduled activity, narrowed for the cross-record agenda.
activity_agenda delivers ThreadActivity rows to a bare assignee through the messaging/thread_activity.read user (assignee) arm — an assignment is itself the grant. Because that assignee holds no read on the parent record, thread, or attachment, this projection exposes only the activity's own fields, the assignee's own identity (user — always the actor, by the agenda filter), and the minimal record pointer (attachment → :class:RecordPointerType); it deliberately drops the parent thread and the attachment's metadata, which a to-one traversal would resolve unguarded and so leak to a bare assignee through the agenda. The record-scoped record_thread surface projects the full :class:ThreadActivityType for a user who can already read the parent record.
state
@strawberry.field
def state() -> strReturn the derived activity state (planned/today/overdue/done/canceled).
attachment
@strawberry.field
def attachment() -> RecordPointerTypeReturn the minimal pointer to the record this activity is attached to.
This is the explicit exception to :func:angee.graphql.relations.actor_scoped_to_one: a narrow-projection contract, not visibility redaction.
The attachment FK is primed elevated and by id on the agenda queryset (:meth:ThreadActivityQuerySet.with_record_pointers), so this reads no extra row; the three pointer facts come from the ThreadAttachment owner without loading or re-gating the target record.
ThreadNotificationType
@strawberry_django.type(ThreadNotification)
class ThreadNotificationType(AngeeNode)GraphQL projection of a per-recipient delivery row (read state lives on receipts).
MessageEdgeType
@strawberry_django.type(MessageEdge)
class MessageEdgeType(AngeeNode)GraphQL projection of a cross-message edge (quote/mention/crosspost/forward).
ReactionType
@strawberry_django.type(Reaction)
class ReactionType(AngeeNode)GraphQL projection of an attributed reaction.
MessageStarType
@strawberry_django.type(MessageStar)
class MessageStarType(AngeeNode)GraphQL projection of a user's starred message marker.
MessageSubtypeOptionType
@strawberry.type
class MessageSubtypeOptionType()Follower-selectable chatter subtype option.
SuggestedRecipientType
@strawberry.type
class SuggestedRecipientType()One Odoo-style suggested recipient for a record chatter composer.
RecordReferenceInput
@strawberry.input
class RecordReferenceInput()Record identity for a model-backed chatter thread.
RecordThreadInput
@strawberry.input
class RecordThreadInput(RecordReferenceInput)Fields accepted when fetching a model-backed chatter thread.
RecordMessagePostInput
@strawberry.input
class RecordMessagePostInput(RecordReferenceInput)Fields accepted when posting an internal chatter message.
RecordMessageUpdateInput
@strawberry.input
class RecordMessageUpdateInput(RecordReferenceInput)Fields accepted when editing an existing chatter comment.
RecordMessageDeleteInput
@strawberry.input
class RecordMessageDeleteInput(RecordReferenceInput)Fields accepted when deleting an existing chatter message.
RecordMessageReactionInput
@strawberry.input
class RecordMessageReactionInput(RecordReferenceInput)Fields accepted when reacting to a chatter message.
RecordMessageStarInput
@strawberry.input
class RecordMessageStarInput(RecordReferenceInput)Fields accepted when starring or unstarring a chatter message.
RecordMessageDoneInput
@strawberry.input
class RecordMessageDoneInput(RecordReferenceInput)Fields accepted when marking a chatter message done for the user.
RecordFollowInput
@strawberry.input
class RecordFollowInput(RecordReferenceInput)Fields accepted when following or unfollowing a record chatter thread.
RecordActivityScheduleInput
@strawberry.input
class RecordActivityScheduleInput(RecordReferenceInput)Fields accepted when scheduling a record activity.
RecordActivityFeedbackInput
@strawberry.input
class RecordActivityFeedbackInput()Fields accepted when completing a record activity.
RecordActivityCancelInput
@strawberry.input
class RecordActivityCancelInput()Fields accepted when canceling a record activity.
RecordThreadPayload
@strawberry.type
class RecordThreadPayload()A record chatter thread, or the error that prevented resolving it.
RecordMessagePostPayload
@strawberry.type
class RecordMessagePostPayload()A posted chatter message, or the error that prevented posting it.
RecordMessageUpdatePayload
@strawberry.type
class RecordMessageUpdatePayload()An updated chatter message, or the error that prevented editing it.
RecordMessageDeletePayload
@strawberry.type
class RecordMessageDeletePayload()A deleted chatter message id plus refreshed thread state, or an error.
RecordMessageReactionPayload
@strawberry.type
class RecordMessageReactionPayload()A reacted chatter message, or the error that prevented reacting.
RecordMessageStarPayload
@strawberry.type
class RecordMessageStarPayload()A starred/unstarred chatter message result, or the error that prevented it.
RecordMessageDonePayload
@strawberry.type
class RecordMessageDonePayload()A message marked done for the user, or the error that prevented it.
RecordFollowPayload
@strawberry.type
class RecordFollowPayload()A record follower update result, or the error that prevented it.
RecordActivityPayload
@strawberry.type
class RecordActivityPayload()A record activity update result, or the error that prevented it.
MessagingQuery
@strawberry.type
class MessagingQuery()Record- and actor-scoped chatter queries.
record_thread
@strawberry.field(name="record_thread")
def record_thread(info: strawberry.Info,
input: RecordThreadInput) -> RecordThreadPayloadReturn the existing chatter thread attached to a model record.
record_thread_unread_count
@strawberry.field(name="record_thread_unread_count")
def record_thread_unread_count(
info: strawberry.Info,
model_label: Annotated[str, strawberry.argument(name="model_label")],
record_id: Annotated[strawberry.ID,
strawberry.argument(name="record_id")]
) -> intReturn the request actor's unread count for a readable record thread.
activity_agenda
@strawberry.field(name="activity_agenda")
def activity_agenda(info: strawberry.Info,
window_start: date,
window_end: date,
include_done: bool = False) -> list[AgendaActivityType]Return the current actor's assigned activities due within the window.
The actor's own agenda across records: ThreadActivity rows assigned to the actor (user), due within [window_start, window_end), ordered by due date. The window is the whole bound — no pagination. Done and canceled are excluded unless include_done. Overdue is neither stored nor filtered; it rides each row's state derivation.
Authorization rides the existing messaging/thread_activity.read user (assignee) arm — an assignment is itself the grant — so the read needs no parent-record re-gate and no record-read fan-out. Each row is projected as :class:AgendaActivityType: the activity's own fields, the actor's own identity, and the minimal record pointer — never the parent thread or attachment metadata, which a to-one traversal would otherwise resolve unguarded. The assignee FK is the request actor by construction, and the pointer attachment is primed elevated and by id on the queryset, so the projection needs no REBAC-guarded select_related (the pitfall) and no per-row pointer fan-out.
MessagingMutation
@strawberry.type
class MessagingMutation()Record-backed chatter mutations.
post_record_message
@strawberry.mutation(name="post_record_message")
def post_record_message(
info: strawberry.Info,
input: RecordMessagePostInput) -> RecordMessagePostPayloadPost an internal comment to the record's chatter thread.
update_record_message
@strawberry.mutation(name="update_record_message")
def update_record_message(
info: strawberry.Info,
input: RecordMessageUpdateInput) -> RecordMessageUpdatePayloadUpdate an existing internal comment in the record's chatter thread.
delete_record_message
@strawberry.mutation(name="delete_record_message")
def delete_record_message(
info: strawberry.Info,
input: RecordMessageDeleteInput) -> RecordMessageDeletePayloadDelete a message from the record's chatter thread.
set_record_message_reaction
@strawberry.mutation(name="set_record_message_reaction")
def set_record_message_reaction(
info: strawberry.Info,
input: RecordMessageReactionInput) -> RecordMessageReactionPayloadAdd, remove, or toggle the current user's reaction on a chatter message.
set_record_message_starred
@strawberry.mutation(name="set_record_message_starred")
def set_record_message_starred(
info: strawberry.Info,
input: RecordMessageStarInput) -> RecordMessageStarPayloadSet or toggle the current user's star on a chatter message.
mark_record_message_done
@strawberry.mutation(name="mark_record_message_done")
def mark_record_message_done(
info: strawberry.Info,
input: RecordMessageDoneInput) -> RecordMessageDonePayloadMark one chatter message done for the current user.
set_record_following
@strawberry.mutation(name="set_record_following")
def set_record_following(info: strawberry.Info,
input: RecordFollowInput) -> RecordFollowPayloadFollow or unfollow a record's chatter thread as the current user.
schedule_record_activity
@strawberry.mutation(name="schedule_record_activity")
def schedule_record_activity(
info: strawberry.Info,
input: RecordActivityScheduleInput) -> RecordActivityPayloadSchedule an activity on the record's chatter thread.
complete_record_activity
@strawberry.mutation(name="complete_record_activity")
def complete_record_activity(
info: strawberry.Info,
input: RecordActivityFeedbackInput) -> RecordActivityPayloadComplete a scheduled record activity and log its feedback.
cancel_record_activity
@strawberry.mutation(name="cancel_record_activity")
def cancel_record_activity(
info: strawberry.Info,
input: RecordActivityCancelInput) -> RecordActivityPayloadCancel a scheduled record activity.
mark_record_thread_read
@strawberry.mutation(name="mark_record_thread_read")
def mark_record_thread_read(
info: strawberry.Info,
input: RecordReferenceInput) -> RecordThreadPayloadMark the current user's notifications on a record thread as read.
_InboxWriteBackend
class _InboxWriteBackend(AngeeHasuraWriteBackend)Write backend that keeps record-attached chatter off the generic mutations.
The read side scopes threads/messages to .inbox() through get_queryset; the write side must match, or a creator who lost record access could still reach a record-attached row through update_<res>_by_pk / delete_<res>_by_pk (its own owner/admin permission would allow it). Narrowing the write-target queryset to .inbox() makes the by-pk lookup miss a record-attached row, so the generic mutation reports it as not found while the inbox rows keep their update/delete surfaces. The .inbox() verb resolves polymorphically on each model's queryset, so one backend serves both.
write_target_queryset
def write_target_queryset() -> AnyScope the update/delete/save target queryset to non-record rows.
_ChannelWriteBackend
class _ChannelWriteBackend(AngeeHasuraWriteBackend)Make the generic delete_channels_by_pk a purge, not an orphan.
delete=True on the channel resource is what lights the console's delete button (the generic views gate on the presence of the delete root). Its default backend would SET_NULL the channel's threads/messages — the shared-Integration-parent orphan this whole feature exists to prevent — so the auto path routes through the same :meth:ChannelManager.purge as the authored delete_channel preview mutation the console actually drives; both are thin dispatchers to that one owner.
delete
def delete(info: strawberry.Info, pk: str) -> AnyPurge, then delete, one public-id-addressed channel; return the deleted row.