ctfy.sdk.resources.messages

client.messages -- the caller's site messages and inbox stream (#2073).

A thread is a private conversation between an event's organisers (or the platform's administrators) and one person or one team. Messages are plain text and kept permanently; reading marks them read and nothing else.

  1"""``client.messages`` -- the caller's site messages and inbox stream (#2073).
  2
  3A thread is a private conversation between an event's organisers (or the
  4platform's administrators) and one person or one team. Messages are plain
  5text and kept permanently; reading marks them read and nothing else.
  6"""
  7
  8from __future__ import annotations
  9
 10from typing import Any
 11
 12from ctfy.sdk._helpers import PagedList, _extract_items, _raise_for_status
 13from ctfy.sdk.base import BaseHttpClient
 14from ctfy.server.models import (
 15    InboxItem,
 16    InboxOverview,
 17    MessageSecrets,
 18    MessageThreadDetail,
 19    ReadAllResult,
 20)
 21
 22
 23def _scope(
 24    competition_id: str, platform: bool, type: str | None, unread: bool = False
 25) -> dict[str, Any]:
 26    params: dict[str, Any] = {}
 27    if competition_id:
 28        params["competition_id"] = competition_id
 29    if platform:
 30        params["platform"] = "true"
 31    if type:
 32        params["type"] = type
 33    if unread:
 34        params["unread"] = "true"
 35    return params
 36
 37
 38class MessagesResource:
 39    """Site messages, as the caller reads them."""
 40
 41    def __init__(self, http: BaseHttpClient) -> None:
 42        self._http = http
 43
 44    def inbox(
 45        self,
 46        *,
 47        competition_id: str = "",
 48        platform: bool = False,
 49        type: str | None = None,
 50        unread: bool = False,
 51        limit: int = 50,
 52        offset: int = 0,
 53    ) -> PagedList[InboxItem]:
 54        """One page of the inbox stream, newest first. ``competition_id``
 55        narrows to one event (plus the platform's critical announcements);
 56        ``type`` is ``conversation`` / ``announcement`` / ``team`` /
 57        ``notice``. Items waiting on the caller are :meth:`overview`'s."""
 58        params = _scope(competition_id, platform, type, unread)
 59        params.update(limit=limit, offset=offset)
 60        resp = self._http.request("GET", "/me/inbox/items", params=params)
 61        _raise_for_status(resp)
 62        return _extract_items(resp.json(), InboxItem)
 63
 64    def overview(
 65        self, *, competition_id: str = "", platform: bool = False, type: str | None = None
 66    ) -> InboxOverview:
 67        """The pinned items (waiting on the caller), the events the caller has
 68        messages in, and the unread count, for the same scope."""
 69        resp = self._http.request(
 70            "GET", "/me/inbox/overview", params=_scope(competition_id, platform, type)
 71        )
 72        _raise_for_status(resp)
 73        return InboxOverview.model_validate(resp.json())
 74
 75    def thread(self, thread_id: str) -> MessageThreadDetail:
 76        """One conversation with every message. Another team's thread is a 404."""
 77        resp = self._http.request("GET", f"/me/messages/threads/{thread_id}")
 78        _raise_for_status(resp)
 79        return MessageThreadDetail.model_validate(resp.json())
 80
 81    def reply(self, thread_id: str, body: str, *, email: bool = False) -> MessageThreadDetail:
 82        """Add a message. A player's reply is plain text of at most 2000
 83        characters, 3 a minute and 50 a day per thread (a 429 says when the
 84        next is allowed). ``email`` asks for a reminder mail and is honoured
 85        for the organisers only."""
 86        resp = self._http.request(
 87            "POST",
 88            f"/me/messages/threads/{thread_id}/messages",
 89            json={"body": body, "email": email},
 90        )
 91        _raise_for_status(resp)
 92        return MessageThreadDetail.model_validate(resp.json())
 93
 94    def open_secrets(self, thread_id: str, message_id: str) -> MessageSecrets:
 95        """The withheld values of one personalised message. Every read is an
 96        audit row."""
 97        resp = self._http.request(
 98            "POST", f"/me/messages/threads/{thread_id}/messages/{message_id}/secrets"
 99        )
100        _raise_for_status(resp)
101        return MessageSecrets.model_validate(resp.json())
102
103    def mark_read(self, thread_id: str) -> ReadAllResult:
104        """Mark one thread read up to now."""
105        resp = self._http.request("POST", f"/me/messages/threads/{thread_id}/read")
106        _raise_for_status(resp)
107        return ReadAllResult.model_validate(resp.json())
108
109    def set_status(self, thread_id: str, status: str) -> MessageThreadDetail:
110        """Organisers: mark a thread ``resolved`` or ``open`` again."""
111        resp = self._http.request(
112            "POST", f"/me/messages/threads/{thread_id}/status", json={"status": status}
113        )
114        _raise_for_status(resp)
115        return MessageThreadDetail.model_validate(resp.json())
116
117    def contact_organisers(
118        self, competition_id: str, subject: str, body: str
119    ) -> MessageThreadDetail:
120        """Open a thread with an event's organisers, as the caller's team."""
121        resp = self._http.request(
122            "POST",
123            f"/competitions/{competition_id}/messages",
124            json={"subject": subject, "body": body},
125        )
126        _raise_for_status(resp)
127        return MessageThreadDetail.model_validate(resp.json())
128
129    def read_all(self, *, competition_id: str = "", platform: bool = False) -> ReadAllResult:
130        """Mark every thread of the scope read."""
131        resp = self._http.request(
132            "POST", "/me/messages/read-all", params=_scope(competition_id, platform, None)
133        )
134        _raise_for_status(resp)
135        return ReadAllResult.model_validate(resp.json())
136
137    def mark_notification_read(self, notification_id: str) -> ReadAllResult:
138        """Mark one in-app notification (an inbox item of kind
139        ``notification``, #2567) read. ``notifications`` is 1 when it changed;
140        an id that is not the caller's changes nothing."""
141        resp = self._http.request("POST", f"/me/inbox/notifications/{notification_id}/read")
142        _raise_for_status(resp)
143        return ReadAllResult.model_validate(resp.json())
144
145    def read_all_notifications(
146        self, *, competition_id: str = "", platform: bool = False
147    ) -> ReadAllResult:
148        """Mark every in-app notification of the scope read (all, one event,
149        or the platform's own)."""
150        resp = self._http.request(
151            "POST",
152            "/me/inbox/notifications/read-all",
153            params=_scope(competition_id, platform, None),
154        )
155        _raise_for_status(resp)
156        return ReadAllResult.model_validate(resp.json())
157
158    def archive(self, competition_id: str) -> ReadAllResult:
159        """Archive an event's messages: read (its notifications too), and
160        hidden by default."""
161        resp = self._http.request("POST", f"/me/messages/archives/{competition_id}")
162        _raise_for_status(resp)
163        return ReadAllResult.model_validate(resp.json())
164
165    def unarchive(self, competition_id: str) -> ReadAllResult:
166        """Bring an archived event's messages back into the inbox."""
167        resp = self._http.request("DELETE", f"/me/messages/archives/{competition_id}")
168        _raise_for_status(resp)
169        return ReadAllResult.model_validate(resp.json())
class MessagesResource:
 39class MessagesResource:
 40    """Site messages, as the caller reads them."""
 41
 42    def __init__(self, http: BaseHttpClient) -> None:
 43        self._http = http
 44
 45    def inbox(
 46        self,
 47        *,
 48        competition_id: str = "",
 49        platform: bool = False,
 50        type: str | None = None,
 51        unread: bool = False,
 52        limit: int = 50,
 53        offset: int = 0,
 54    ) -> PagedList[InboxItem]:
 55        """One page of the inbox stream, newest first. ``competition_id``
 56        narrows to one event (plus the platform's critical announcements);
 57        ``type`` is ``conversation`` / ``announcement`` / ``team`` /
 58        ``notice``. Items waiting on the caller are :meth:`overview`'s."""
 59        params = _scope(competition_id, platform, type, unread)
 60        params.update(limit=limit, offset=offset)
 61        resp = self._http.request("GET", "/me/inbox/items", params=params)
 62        _raise_for_status(resp)
 63        return _extract_items(resp.json(), InboxItem)
 64
 65    def overview(
 66        self, *, competition_id: str = "", platform: bool = False, type: str | None = None
 67    ) -> InboxOverview:
 68        """The pinned items (waiting on the caller), the events the caller has
 69        messages in, and the unread count, for the same scope."""
 70        resp = self._http.request(
 71            "GET", "/me/inbox/overview", params=_scope(competition_id, platform, type)
 72        )
 73        _raise_for_status(resp)
 74        return InboxOverview.model_validate(resp.json())
 75
 76    def thread(self, thread_id: str) -> MessageThreadDetail:
 77        """One conversation with every message. Another team's thread is a 404."""
 78        resp = self._http.request("GET", f"/me/messages/threads/{thread_id}")
 79        _raise_for_status(resp)
 80        return MessageThreadDetail.model_validate(resp.json())
 81
 82    def reply(self, thread_id: str, body: str, *, email: bool = False) -> MessageThreadDetail:
 83        """Add a message. A player's reply is plain text of at most 2000
 84        characters, 3 a minute and 50 a day per thread (a 429 says when the
 85        next is allowed). ``email`` asks for a reminder mail and is honoured
 86        for the organisers only."""
 87        resp = self._http.request(
 88            "POST",
 89            f"/me/messages/threads/{thread_id}/messages",
 90            json={"body": body, "email": email},
 91        )
 92        _raise_for_status(resp)
 93        return MessageThreadDetail.model_validate(resp.json())
 94
 95    def open_secrets(self, thread_id: str, message_id: str) -> MessageSecrets:
 96        """The withheld values of one personalised message. Every read is an
 97        audit row."""
 98        resp = self._http.request(
 99            "POST", f"/me/messages/threads/{thread_id}/messages/{message_id}/secrets"
100        )
101        _raise_for_status(resp)
102        return MessageSecrets.model_validate(resp.json())
103
104    def mark_read(self, thread_id: str) -> ReadAllResult:
105        """Mark one thread read up to now."""
106        resp = self._http.request("POST", f"/me/messages/threads/{thread_id}/read")
107        _raise_for_status(resp)
108        return ReadAllResult.model_validate(resp.json())
109
110    def set_status(self, thread_id: str, status: str) -> MessageThreadDetail:
111        """Organisers: mark a thread ``resolved`` or ``open`` again."""
112        resp = self._http.request(
113            "POST", f"/me/messages/threads/{thread_id}/status", json={"status": status}
114        )
115        _raise_for_status(resp)
116        return MessageThreadDetail.model_validate(resp.json())
117
118    def contact_organisers(
119        self, competition_id: str, subject: str, body: str
120    ) -> MessageThreadDetail:
121        """Open a thread with an event's organisers, as the caller's team."""
122        resp = self._http.request(
123            "POST",
124            f"/competitions/{competition_id}/messages",
125            json={"subject": subject, "body": body},
126        )
127        _raise_for_status(resp)
128        return MessageThreadDetail.model_validate(resp.json())
129
130    def read_all(self, *, competition_id: str = "", platform: bool = False) -> ReadAllResult:
131        """Mark every thread of the scope read."""
132        resp = self._http.request(
133            "POST", "/me/messages/read-all", params=_scope(competition_id, platform, None)
134        )
135        _raise_for_status(resp)
136        return ReadAllResult.model_validate(resp.json())
137
138    def mark_notification_read(self, notification_id: str) -> ReadAllResult:
139        """Mark one in-app notification (an inbox item of kind
140        ``notification``, #2567) read. ``notifications`` is 1 when it changed;
141        an id that is not the caller's changes nothing."""
142        resp = self._http.request("POST", f"/me/inbox/notifications/{notification_id}/read")
143        _raise_for_status(resp)
144        return ReadAllResult.model_validate(resp.json())
145
146    def read_all_notifications(
147        self, *, competition_id: str = "", platform: bool = False
148    ) -> ReadAllResult:
149        """Mark every in-app notification of the scope read (all, one event,
150        or the platform's own)."""
151        resp = self._http.request(
152            "POST",
153            "/me/inbox/notifications/read-all",
154            params=_scope(competition_id, platform, None),
155        )
156        _raise_for_status(resp)
157        return ReadAllResult.model_validate(resp.json())
158
159    def archive(self, competition_id: str) -> ReadAllResult:
160        """Archive an event's messages: read (its notifications too), and
161        hidden by default."""
162        resp = self._http.request("POST", f"/me/messages/archives/{competition_id}")
163        _raise_for_status(resp)
164        return ReadAllResult.model_validate(resp.json())
165
166    def unarchive(self, competition_id: str) -> ReadAllResult:
167        """Bring an archived event's messages back into the inbox."""
168        resp = self._http.request("DELETE", f"/me/messages/archives/{competition_id}")
169        _raise_for_status(resp)
170        return ReadAllResult.model_validate(resp.json())

Site messages, as the caller reads them.

MessagesResource(http: ctfy.sdk.base.BaseHttpClient)
42    def __init__(self, http: BaseHttpClient) -> None:
43        self._http = http
def inbox( self, *, competition_id: str = '', platform: bool = False, type: str | None = None, unread: bool = False, limit: int = 50, offset: int = 0) -> ctfy.sdk._helpers.PagedList[ctfy.server.models.InboxItem]:
45    def inbox(
46        self,
47        *,
48        competition_id: str = "",
49        platform: bool = False,
50        type: str | None = None,
51        unread: bool = False,
52        limit: int = 50,
53        offset: int = 0,
54    ) -> PagedList[InboxItem]:
55        """One page of the inbox stream, newest first. ``competition_id``
56        narrows to one event (plus the platform's critical announcements);
57        ``type`` is ``conversation`` / ``announcement`` / ``team`` /
58        ``notice``. Items waiting on the caller are :meth:`overview`'s."""
59        params = _scope(competition_id, platform, type, unread)
60        params.update(limit=limit, offset=offset)
61        resp = self._http.request("GET", "/me/inbox/items", params=params)
62        _raise_for_status(resp)
63        return _extract_items(resp.json(), InboxItem)

One page of the inbox stream, newest first. competition_id narrows to one event (plus the platform's critical announcements); type is conversation / announcement / team / notice. Items waiting on the caller are overview()'s.

def overview( self, *, competition_id: str = '', platform: bool = False, type: str | None = None) -> ctfy.server.models.InboxOverview:
65    def overview(
66        self, *, competition_id: str = "", platform: bool = False, type: str | None = None
67    ) -> InboxOverview:
68        """The pinned items (waiting on the caller), the events the caller has
69        messages in, and the unread count, for the same scope."""
70        resp = self._http.request(
71            "GET", "/me/inbox/overview", params=_scope(competition_id, platform, type)
72        )
73        _raise_for_status(resp)
74        return InboxOverview.model_validate(resp.json())

The pinned items (waiting on the caller), the events the caller has messages in, and the unread count, for the same scope.

def thread(self, thread_id: str) -> ctfy.server.models.MessageThreadDetail:
76    def thread(self, thread_id: str) -> MessageThreadDetail:
77        """One conversation with every message. Another team's thread is a 404."""
78        resp = self._http.request("GET", f"/me/messages/threads/{thread_id}")
79        _raise_for_status(resp)
80        return MessageThreadDetail.model_validate(resp.json())

One conversation with every message. Another team's thread is a 404.

def reply( self, thread_id: str, body: str, *, email: bool = False) -> ctfy.server.models.MessageThreadDetail:
82    def reply(self, thread_id: str, body: str, *, email: bool = False) -> MessageThreadDetail:
83        """Add a message. A player's reply is plain text of at most 2000
84        characters, 3 a minute and 50 a day per thread (a 429 says when the
85        next is allowed). ``email`` asks for a reminder mail and is honoured
86        for the organisers only."""
87        resp = self._http.request(
88            "POST",
89            f"/me/messages/threads/{thread_id}/messages",
90            json={"body": body, "email": email},
91        )
92        _raise_for_status(resp)
93        return MessageThreadDetail.model_validate(resp.json())

Add a message. A player's reply is plain text of at most 2000 characters, 3 a minute and 50 a day per thread (a 429 says when the next is allowed). email asks for a reminder mail and is honoured for the organisers only.

def open_secrets( self, thread_id: str, message_id: str) -> ctfy.server.models.MessageSecrets:
 95    def open_secrets(self, thread_id: str, message_id: str) -> MessageSecrets:
 96        """The withheld values of one personalised message. Every read is an
 97        audit row."""
 98        resp = self._http.request(
 99            "POST", f"/me/messages/threads/{thread_id}/messages/{message_id}/secrets"
100        )
101        _raise_for_status(resp)
102        return MessageSecrets.model_validate(resp.json())

The withheld values of one personalised message. Every read is an audit row.

def mark_read(self, thread_id: str) -> ctfy.server.models.ReadAllResult:
104    def mark_read(self, thread_id: str) -> ReadAllResult:
105        """Mark one thread read up to now."""
106        resp = self._http.request("POST", f"/me/messages/threads/{thread_id}/read")
107        _raise_for_status(resp)
108        return ReadAllResult.model_validate(resp.json())

Mark one thread read up to now.

def set_status( self, thread_id: str, status: str) -> ctfy.server.models.MessageThreadDetail:
110    def set_status(self, thread_id: str, status: str) -> MessageThreadDetail:
111        """Organisers: mark a thread ``resolved`` or ``open`` again."""
112        resp = self._http.request(
113            "POST", f"/me/messages/threads/{thread_id}/status", json={"status": status}
114        )
115        _raise_for_status(resp)
116        return MessageThreadDetail.model_validate(resp.json())

Organisers: mark a thread resolved or open again.

def contact_organisers( self, competition_id: str, subject: str, body: str) -> ctfy.server.models.MessageThreadDetail:
118    def contact_organisers(
119        self, competition_id: str, subject: str, body: str
120    ) -> MessageThreadDetail:
121        """Open a thread with an event's organisers, as the caller's team."""
122        resp = self._http.request(
123            "POST",
124            f"/competitions/{competition_id}/messages",
125            json={"subject": subject, "body": body},
126        )
127        _raise_for_status(resp)
128        return MessageThreadDetail.model_validate(resp.json())

Open a thread with an event's organisers, as the caller's team.

def read_all( self, *, competition_id: str = '', platform: bool = False) -> ctfy.server.models.ReadAllResult:
130    def read_all(self, *, competition_id: str = "", platform: bool = False) -> ReadAllResult:
131        """Mark every thread of the scope read."""
132        resp = self._http.request(
133            "POST", "/me/messages/read-all", params=_scope(competition_id, platform, None)
134        )
135        _raise_for_status(resp)
136        return ReadAllResult.model_validate(resp.json())

Mark every thread of the scope read.

def mark_notification_read(self, notification_id: str) -> ctfy.server.models.ReadAllResult:
138    def mark_notification_read(self, notification_id: str) -> ReadAllResult:
139        """Mark one in-app notification (an inbox item of kind
140        ``notification``, #2567) read. ``notifications`` is 1 when it changed;
141        an id that is not the caller's changes nothing."""
142        resp = self._http.request("POST", f"/me/inbox/notifications/{notification_id}/read")
143        _raise_for_status(resp)
144        return ReadAllResult.model_validate(resp.json())

Mark one in-app notification (an inbox item of kind notification, #2567) read. notifications is 1 when it changed; an id that is not the caller's changes nothing.

def read_all_notifications( self, *, competition_id: str = '', platform: bool = False) -> ctfy.server.models.ReadAllResult:
146    def read_all_notifications(
147        self, *, competition_id: str = "", platform: bool = False
148    ) -> ReadAllResult:
149        """Mark every in-app notification of the scope read (all, one event,
150        or the platform's own)."""
151        resp = self._http.request(
152            "POST",
153            "/me/inbox/notifications/read-all",
154            params=_scope(competition_id, platform, None),
155        )
156        _raise_for_status(resp)
157        return ReadAllResult.model_validate(resp.json())

Mark every in-app notification of the scope read (all, one event, or the platform's own).

def archive(self, competition_id: str) -> ctfy.server.models.ReadAllResult:
159    def archive(self, competition_id: str) -> ReadAllResult:
160        """Archive an event's messages: read (its notifications too), and
161        hidden by default."""
162        resp = self._http.request("POST", f"/me/messages/archives/{competition_id}")
163        _raise_for_status(resp)
164        return ReadAllResult.model_validate(resp.json())

Archive an event's messages: read (its notifications too), and hidden by default.

def unarchive(self, competition_id: str) -> ctfy.server.models.ReadAllResult:
166    def unarchive(self, competition_id: str) -> ReadAllResult:
167        """Bring an archived event's messages back into the inbox."""
168        resp = self._http.request("DELETE", f"/me/messages/archives/{competition_id}")
169        _raise_for_status(resp)
170        return ReadAllResult.model_validate(resp.json())

Bring an archived event's messages back into the inbox.