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())
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.