ctfy.sdk.admin_resources.messages

client.admin.messages -- the organisers' side of site messages (#2073).

An event's organisers (global admins and the event's own competition admins) send to one entrant, one team, a set of teams or every team -- one thread per team -- and read every thread of their event. Platform administrators also write to any one account (#2064). A thread is read and answered through client.messages, which gives a staff reader the organiser side.

  1"""``client.admin.messages`` -- the organisers' side of site messages (#2073).
  2
  3An event's organisers (global admins and the event's own competition admins)
  4send to one entrant, one team, a set of teams or every team -- one thread per
  5team -- and read every thread of their event. Platform administrators also
  6write to any one account (#2064). A thread is read and answered through
  7``client.messages``, which gives a staff reader the organiser side.
  8"""
  9
 10from __future__ import annotations
 11
 12from collections.abc import Sequence
 13from typing import Any
 14
 15from ctfy.sdk._helpers import PagedList, _extract_items, _raise_for_status
 16from ctfy.sdk.base import BaseHttpClient
 17from ctfy.server.models import (
 18    AudiencePreview,
 19    ComposeResult,
 20    MessageTeamOption,
 21    MessageThreadRow,
 22    PersonalisedPreview,
 23)
 24
 25
 26class AdminMessagesResource:
 27    def __init__(self, http: BaseHttpClient) -> None:
 28        self._http = http
 29
 30    def threads(
 31        self,
 32        competition_id: str,
 33        *,
 34        status: str = "",
 35        awaiting: str = "",
 36        kind: str = "",
 37        team_id: str = "",
 38        unread: bool = False,
 39        limit: int = 50,
 40        offset: int = 0,
 41    ) -> PagedList[MessageThreadRow]:
 42        """Every thread of the event, newest activity first. ``awaiting=
 43        "organiser"`` is the queue of questions waiting on an answer."""
 44        filters = {"status": status, "awaiting": awaiting, "kind": kind, "team_id": team_id}
 45        params: dict[str, Any] = {
 46            "limit": limit,
 47            "offset": offset,
 48            **{k: v for k, v in filters.items() if v},
 49        }
 50        if unread:
 51            params["unread"] = "true"
 52        resp = self._http.request(
 53            "GET", f"/admin/competitions/{competition_id}/messages/threads", params=params
 54        )
 55        _raise_for_status(resp)
 56        return _extract_items(resp.json(), MessageThreadRow)
 57
 58    def teams(
 59        self, competition_id: str, *, q: str = "", limit: int = 50, offset: int = 0
 60    ) -> PagedList[MessageTeamOption]:
 61        """The event's teams as a message may name them (anonymous teams by
 62        their public label)."""
 63        resp = self._http.request(
 64            "GET",
 65            f"/admin/competitions/{competition_id}/messages/teams",
 66            params={"q": q, "limit": limit, "offset": offset},
 67        )
 68        _raise_for_status(resp)
 69        return _extract_items(resp.json(), MessageTeamOption)
 70
 71    def audience(
 72        self,
 73        competition_id: str,
 74        audience: str,
 75        *,
 76        team_ids: Sequence[str] = (),
 77        user_id: str = "",
 78        conditions: Sequence[str] = (),
 79    ) -> AudiencePreview:
 80        """How many threads and readers a send would make."""
 81        resp = self._http.request(
 82            "POST",
 83            f"/admin/competitions/{competition_id}/messages/audience",
 84            json={
 85                "audience": audience,
 86                "team_ids": list(team_ids),
 87                "user_id": user_id,
 88                "conditions": list(conditions),
 89            },
 90        )
 91        _raise_for_status(resp)
 92        return AudiencePreview.model_validate(resp.json())
 93
 94    def send(
 95        self,
 96        competition_id: str,
 97        *,
 98        audience: str,
 99        subject: str,
100        body: str,
101        team_ids: Sequence[str] = (),
102        user_id: str = "",
103        conditions: Sequence[str] = (),
104        email: bool = False,
105        allow_reply: bool = True,
106    ) -> ComposeResult:
107        """Send from the event's organisers. ``audience`` is ``user`` /
108        ``team`` / ``teams`` / ``all_teams``. With ``teams``, ``conditions`` (``all`` /
109        ``report_missing`` / ``review_not_approved`` / ``no_solves``) add the
110        teams meeting any of them, resolved by the server when sent, to the
111        named ``team_ids`` (a team in both counts once); plain text, subject at most 120
112        characters, body at most 5000. Sent messages are never withdrawn."""
113        resp = self._http.request(
114            "POST",
115            f"/admin/competitions/{competition_id}/messages",
116            json={
117                "audience": audience,
118                "team_ids": list(team_ids),
119                "user_id": user_id,
120                "conditions": list(conditions),
121                "subject": subject,
122                "body": body,
123                "email": email,
124                "allow_reply": allow_reply,
125            },
126        )
127        _raise_for_status(resp)
128        return ComposeResult.model_validate(resp.json())
129
130    def _personalised_body(
131        self,
132        csv_text: str,
133        *,
134        team_column: str,
135        subject: str,
136        body: str,
137        secret_columns: Sequence[str],
138        recipients: str,
139        email: bool,
140        allow_reply: bool,
141        overrides: dict[int, str] | None,
142        skip_lines: Sequence[int],
143    ) -> dict[str, Any]:
144        return {
145            "csv": csv_text,
146            "team_column": team_column,
147            "secret_columns": list(secret_columns),
148            "subject": subject,
149            "body": body,
150            "recipients": recipients,
151            "email": email,
152            "allow_reply": allow_reply,
153            "overrides": {str(k): v for k, v in (overrides or {}).items()},
154            "skip_lines": list(skip_lines),
155        }
156
157    def preview_personalised(
158        self,
159        competition_id: str,
160        csv_text: str,
161        *,
162        team_column: str,
163        subject: str,
164        body: str,
165        secret_columns: Sequence[str] = (),
166        recipients: str = "team",
167        email: bool = False,
168        allow_reply: bool = True,
169        overrides: dict[int, str] | None = None,
170        skip_lines: Sequence[int] = (),
171    ) -> PersonalisedPreview:
172        """What a per-team send from a CSV would do. Sends nothing."""
173        resp = self._http.request(
174            "POST",
175            f"/admin/competitions/{competition_id}/messages/personalised/preview",
176            json=self._personalised_body(
177                csv_text,
178                team_column=team_column,
179                subject=subject,
180                body=body,
181                secret_columns=secret_columns,
182                recipients=recipients,
183                email=email,
184                allow_reply=allow_reply,
185                overrides=overrides,
186                skip_lines=skip_lines,
187            ),
188        )
189        _raise_for_status(resp)
190        return PersonalisedPreview.model_validate(resp.json())
191
192    def send_personalised(
193        self,
194        competition_id: str,
195        csv_text: str,
196        *,
197        team_column: str,
198        subject: str,
199        body: str,
200        secret_columns: Sequence[str] = (),
201        recipients: str = "team",
202        email: bool = False,
203        allow_reply: bool = True,
204        overrides: dict[int, str] | None = None,
205        skip_lines: Sequence[int] = (),
206    ) -> ComposeResult:
207        """Send each matched team its own message from the CSV; secret columns
208        are sealed and shown masked until a reader opens them (audited)."""
209        resp = self._http.request(
210            "POST",
211            f"/admin/competitions/{competition_id}/messages/personalised",
212            json=self._personalised_body(
213                csv_text,
214                team_column=team_column,
215                subject=subject,
216                body=body,
217                secret_columns=secret_columns,
218                recipients=recipients,
219                email=email,
220                allow_reply=allow_reply,
221                overrides=overrides,
222                skip_lines=skip_lines,
223            ),
224        )
225        _raise_for_status(resp)
226        return ComposeResult.model_validate(resp.json())
227
228    def platform_threads(
229        self, *, awaiting: str = "", unread: bool = False, limit: int = 50, offset: int = 0
230    ) -> PagedList[MessageThreadRow]:
231        """Platform administrators: the platform's own threads."""
232        params: dict[str, Any] = {"limit": limit, "offset": offset}
233        if awaiting:
234            params["awaiting"] = awaiting
235        if unread:
236            params["unread"] = "true"
237        resp = self._http.request("GET", "/admin/messages/threads", params=params)
238        _raise_for_status(resp)
239        return _extract_items(resp.json(), MessageThreadRow)
240
241    def send_to_account(
242        self,
243        *,
244        subject: str,
245        body: str,
246        user_id: str = "",
247        email: str = "",
248        send_email: bool = False,
249        allow_reply: bool = True,
250    ) -> ComposeResult:
251        """Platform administrators: write to one account, by id or address."""
252        resp = self._http.request(
253            "POST",
254            "/admin/messages",
255            json={
256                "user_id": user_id,
257                "email": email,
258                "subject": subject,
259                "body": body,
260                "send_email": send_email,
261                "allow_reply": allow_reply,
262            },
263        )
264        _raise_for_status(resp)
265        return ComposeResult.model_validate(resp.json())
class AdminMessagesResource:
 27class AdminMessagesResource:
 28    def __init__(self, http: BaseHttpClient) -> None:
 29        self._http = http
 30
 31    def threads(
 32        self,
 33        competition_id: str,
 34        *,
 35        status: str = "",
 36        awaiting: str = "",
 37        kind: str = "",
 38        team_id: str = "",
 39        unread: bool = False,
 40        limit: int = 50,
 41        offset: int = 0,
 42    ) -> PagedList[MessageThreadRow]:
 43        """Every thread of the event, newest activity first. ``awaiting=
 44        "organiser"`` is the queue of questions waiting on an answer."""
 45        filters = {"status": status, "awaiting": awaiting, "kind": kind, "team_id": team_id}
 46        params: dict[str, Any] = {
 47            "limit": limit,
 48            "offset": offset,
 49            **{k: v for k, v in filters.items() if v},
 50        }
 51        if unread:
 52            params["unread"] = "true"
 53        resp = self._http.request(
 54            "GET", f"/admin/competitions/{competition_id}/messages/threads", params=params
 55        )
 56        _raise_for_status(resp)
 57        return _extract_items(resp.json(), MessageThreadRow)
 58
 59    def teams(
 60        self, competition_id: str, *, q: str = "", limit: int = 50, offset: int = 0
 61    ) -> PagedList[MessageTeamOption]:
 62        """The event's teams as a message may name them (anonymous teams by
 63        their public label)."""
 64        resp = self._http.request(
 65            "GET",
 66            f"/admin/competitions/{competition_id}/messages/teams",
 67            params={"q": q, "limit": limit, "offset": offset},
 68        )
 69        _raise_for_status(resp)
 70        return _extract_items(resp.json(), MessageTeamOption)
 71
 72    def audience(
 73        self,
 74        competition_id: str,
 75        audience: str,
 76        *,
 77        team_ids: Sequence[str] = (),
 78        user_id: str = "",
 79        conditions: Sequence[str] = (),
 80    ) -> AudiencePreview:
 81        """How many threads and readers a send would make."""
 82        resp = self._http.request(
 83            "POST",
 84            f"/admin/competitions/{competition_id}/messages/audience",
 85            json={
 86                "audience": audience,
 87                "team_ids": list(team_ids),
 88                "user_id": user_id,
 89                "conditions": list(conditions),
 90            },
 91        )
 92        _raise_for_status(resp)
 93        return AudiencePreview.model_validate(resp.json())
 94
 95    def send(
 96        self,
 97        competition_id: str,
 98        *,
 99        audience: str,
100        subject: str,
101        body: str,
102        team_ids: Sequence[str] = (),
103        user_id: str = "",
104        conditions: Sequence[str] = (),
105        email: bool = False,
106        allow_reply: bool = True,
107    ) -> ComposeResult:
108        """Send from the event's organisers. ``audience`` is ``user`` /
109        ``team`` / ``teams`` / ``all_teams``. With ``teams``, ``conditions`` (``all`` /
110        ``report_missing`` / ``review_not_approved`` / ``no_solves``) add the
111        teams meeting any of them, resolved by the server when sent, to the
112        named ``team_ids`` (a team in both counts once); plain text, subject at most 120
113        characters, body at most 5000. Sent messages are never withdrawn."""
114        resp = self._http.request(
115            "POST",
116            f"/admin/competitions/{competition_id}/messages",
117            json={
118                "audience": audience,
119                "team_ids": list(team_ids),
120                "user_id": user_id,
121                "conditions": list(conditions),
122                "subject": subject,
123                "body": body,
124                "email": email,
125                "allow_reply": allow_reply,
126            },
127        )
128        _raise_for_status(resp)
129        return ComposeResult.model_validate(resp.json())
130
131    def _personalised_body(
132        self,
133        csv_text: str,
134        *,
135        team_column: str,
136        subject: str,
137        body: str,
138        secret_columns: Sequence[str],
139        recipients: str,
140        email: bool,
141        allow_reply: bool,
142        overrides: dict[int, str] | None,
143        skip_lines: Sequence[int],
144    ) -> dict[str, Any]:
145        return {
146            "csv": csv_text,
147            "team_column": team_column,
148            "secret_columns": list(secret_columns),
149            "subject": subject,
150            "body": body,
151            "recipients": recipients,
152            "email": email,
153            "allow_reply": allow_reply,
154            "overrides": {str(k): v for k, v in (overrides or {}).items()},
155            "skip_lines": list(skip_lines),
156        }
157
158    def preview_personalised(
159        self,
160        competition_id: str,
161        csv_text: str,
162        *,
163        team_column: str,
164        subject: str,
165        body: str,
166        secret_columns: Sequence[str] = (),
167        recipients: str = "team",
168        email: bool = False,
169        allow_reply: bool = True,
170        overrides: dict[int, str] | None = None,
171        skip_lines: Sequence[int] = (),
172    ) -> PersonalisedPreview:
173        """What a per-team send from a CSV would do. Sends nothing."""
174        resp = self._http.request(
175            "POST",
176            f"/admin/competitions/{competition_id}/messages/personalised/preview",
177            json=self._personalised_body(
178                csv_text,
179                team_column=team_column,
180                subject=subject,
181                body=body,
182                secret_columns=secret_columns,
183                recipients=recipients,
184                email=email,
185                allow_reply=allow_reply,
186                overrides=overrides,
187                skip_lines=skip_lines,
188            ),
189        )
190        _raise_for_status(resp)
191        return PersonalisedPreview.model_validate(resp.json())
192
193    def send_personalised(
194        self,
195        competition_id: str,
196        csv_text: str,
197        *,
198        team_column: str,
199        subject: str,
200        body: str,
201        secret_columns: Sequence[str] = (),
202        recipients: str = "team",
203        email: bool = False,
204        allow_reply: bool = True,
205        overrides: dict[int, str] | None = None,
206        skip_lines: Sequence[int] = (),
207    ) -> ComposeResult:
208        """Send each matched team its own message from the CSV; secret columns
209        are sealed and shown masked until a reader opens them (audited)."""
210        resp = self._http.request(
211            "POST",
212            f"/admin/competitions/{competition_id}/messages/personalised",
213            json=self._personalised_body(
214                csv_text,
215                team_column=team_column,
216                subject=subject,
217                body=body,
218                secret_columns=secret_columns,
219                recipients=recipients,
220                email=email,
221                allow_reply=allow_reply,
222                overrides=overrides,
223                skip_lines=skip_lines,
224            ),
225        )
226        _raise_for_status(resp)
227        return ComposeResult.model_validate(resp.json())
228
229    def platform_threads(
230        self, *, awaiting: str = "", unread: bool = False, limit: int = 50, offset: int = 0
231    ) -> PagedList[MessageThreadRow]:
232        """Platform administrators: the platform's own threads."""
233        params: dict[str, Any] = {"limit": limit, "offset": offset}
234        if awaiting:
235            params["awaiting"] = awaiting
236        if unread:
237            params["unread"] = "true"
238        resp = self._http.request("GET", "/admin/messages/threads", params=params)
239        _raise_for_status(resp)
240        return _extract_items(resp.json(), MessageThreadRow)
241
242    def send_to_account(
243        self,
244        *,
245        subject: str,
246        body: str,
247        user_id: str = "",
248        email: str = "",
249        send_email: bool = False,
250        allow_reply: bool = True,
251    ) -> ComposeResult:
252        """Platform administrators: write to one account, by id or address."""
253        resp = self._http.request(
254            "POST",
255            "/admin/messages",
256            json={
257                "user_id": user_id,
258                "email": email,
259                "subject": subject,
260                "body": body,
261                "send_email": send_email,
262                "allow_reply": allow_reply,
263            },
264        )
265        _raise_for_status(resp)
266        return ComposeResult.model_validate(resp.json())
AdminMessagesResource(http: ctfy.sdk.base.BaseHttpClient)
28    def __init__(self, http: BaseHttpClient) -> None:
29        self._http = http
def threads( self, competition_id: str, *, status: str = '', awaiting: str = '', kind: str = '', team_id: str = '', unread: bool = False, limit: int = 50, offset: int = 0) -> ctfy.sdk._helpers.PagedList[ctfy.server.models.MessageThreadRow]:
31    def threads(
32        self,
33        competition_id: str,
34        *,
35        status: str = "",
36        awaiting: str = "",
37        kind: str = "",
38        team_id: str = "",
39        unread: bool = False,
40        limit: int = 50,
41        offset: int = 0,
42    ) -> PagedList[MessageThreadRow]:
43        """Every thread of the event, newest activity first. ``awaiting=
44        "organiser"`` is the queue of questions waiting on an answer."""
45        filters = {"status": status, "awaiting": awaiting, "kind": kind, "team_id": team_id}
46        params: dict[str, Any] = {
47            "limit": limit,
48            "offset": offset,
49            **{k: v for k, v in filters.items() if v},
50        }
51        if unread:
52            params["unread"] = "true"
53        resp = self._http.request(
54            "GET", f"/admin/competitions/{competition_id}/messages/threads", params=params
55        )
56        _raise_for_status(resp)
57        return _extract_items(resp.json(), MessageThreadRow)

Every thread of the event, newest activity first. awaiting= "organiser" is the queue of questions waiting on an answer.

def teams( self, competition_id: str, *, q: str = '', limit: int = 50, offset: int = 0) -> ctfy.sdk._helpers.PagedList[ctfy.server.models.MessageTeamOption]:
59    def teams(
60        self, competition_id: str, *, q: str = "", limit: int = 50, offset: int = 0
61    ) -> PagedList[MessageTeamOption]:
62        """The event's teams as a message may name them (anonymous teams by
63        their public label)."""
64        resp = self._http.request(
65            "GET",
66            f"/admin/competitions/{competition_id}/messages/teams",
67            params={"q": q, "limit": limit, "offset": offset},
68        )
69        _raise_for_status(resp)
70        return _extract_items(resp.json(), MessageTeamOption)

The event's teams as a message may name them (anonymous teams by their public label).

def audience( self, competition_id: str, audience: str, *, team_ids: Sequence[str] = (), user_id: str = '', conditions: Sequence[str] = ()) -> ctfy.server.models.AudiencePreview:
72    def audience(
73        self,
74        competition_id: str,
75        audience: str,
76        *,
77        team_ids: Sequence[str] = (),
78        user_id: str = "",
79        conditions: Sequence[str] = (),
80    ) -> AudiencePreview:
81        """How many threads and readers a send would make."""
82        resp = self._http.request(
83            "POST",
84            f"/admin/competitions/{competition_id}/messages/audience",
85            json={
86                "audience": audience,
87                "team_ids": list(team_ids),
88                "user_id": user_id,
89                "conditions": list(conditions),
90            },
91        )
92        _raise_for_status(resp)
93        return AudiencePreview.model_validate(resp.json())

How many threads and readers a send would make.

def send( self, competition_id: str, *, audience: str, subject: str, body: str, team_ids: Sequence[str] = (), user_id: str = '', conditions: Sequence[str] = (), email: bool = False, allow_reply: bool = True) -> ctfy.server.models.ComposeResult:
 95    def send(
 96        self,
 97        competition_id: str,
 98        *,
 99        audience: str,
100        subject: str,
101        body: str,
102        team_ids: Sequence[str] = (),
103        user_id: str = "",
104        conditions: Sequence[str] = (),
105        email: bool = False,
106        allow_reply: bool = True,
107    ) -> ComposeResult:
108        """Send from the event's organisers. ``audience`` is ``user`` /
109        ``team`` / ``teams`` / ``all_teams``. With ``teams``, ``conditions`` (``all`` /
110        ``report_missing`` / ``review_not_approved`` / ``no_solves``) add the
111        teams meeting any of them, resolved by the server when sent, to the
112        named ``team_ids`` (a team in both counts once); plain text, subject at most 120
113        characters, body at most 5000. Sent messages are never withdrawn."""
114        resp = self._http.request(
115            "POST",
116            f"/admin/competitions/{competition_id}/messages",
117            json={
118                "audience": audience,
119                "team_ids": list(team_ids),
120                "user_id": user_id,
121                "conditions": list(conditions),
122                "subject": subject,
123                "body": body,
124                "email": email,
125                "allow_reply": allow_reply,
126            },
127        )
128        _raise_for_status(resp)
129        return ComposeResult.model_validate(resp.json())

Send from the event's organisers. audience is user / team / teams / all_teams. With teams, conditions (all / report_missing / review_not_approved / no_solves) add the teams meeting any of them, resolved by the server when sent, to the named team_ids (a team in both counts once); plain text, subject at most 120 characters, body at most 5000. Sent messages are never withdrawn.

def preview_personalised( self, competition_id: str, csv_text: str, *, team_column: str, subject: str, body: str, secret_columns: Sequence[str] = (), recipients: str = 'team', email: bool = False, allow_reply: bool = True, overrides: dict[int, str] | None = None, skip_lines: Sequence[int] = ()) -> ctfy.server.models.PersonalisedPreview:
158    def preview_personalised(
159        self,
160        competition_id: str,
161        csv_text: str,
162        *,
163        team_column: str,
164        subject: str,
165        body: str,
166        secret_columns: Sequence[str] = (),
167        recipients: str = "team",
168        email: bool = False,
169        allow_reply: bool = True,
170        overrides: dict[int, str] | None = None,
171        skip_lines: Sequence[int] = (),
172    ) -> PersonalisedPreview:
173        """What a per-team send from a CSV would do. Sends nothing."""
174        resp = self._http.request(
175            "POST",
176            f"/admin/competitions/{competition_id}/messages/personalised/preview",
177            json=self._personalised_body(
178                csv_text,
179                team_column=team_column,
180                subject=subject,
181                body=body,
182                secret_columns=secret_columns,
183                recipients=recipients,
184                email=email,
185                allow_reply=allow_reply,
186                overrides=overrides,
187                skip_lines=skip_lines,
188            ),
189        )
190        _raise_for_status(resp)
191        return PersonalisedPreview.model_validate(resp.json())

What a per-team send from a CSV would do. Sends nothing.

def send_personalised( self, competition_id: str, csv_text: str, *, team_column: str, subject: str, body: str, secret_columns: Sequence[str] = (), recipients: str = 'team', email: bool = False, allow_reply: bool = True, overrides: dict[int, str] | None = None, skip_lines: Sequence[int] = ()) -> ctfy.server.models.ComposeResult:
193    def send_personalised(
194        self,
195        competition_id: str,
196        csv_text: str,
197        *,
198        team_column: str,
199        subject: str,
200        body: str,
201        secret_columns: Sequence[str] = (),
202        recipients: str = "team",
203        email: bool = False,
204        allow_reply: bool = True,
205        overrides: dict[int, str] | None = None,
206        skip_lines: Sequence[int] = (),
207    ) -> ComposeResult:
208        """Send each matched team its own message from the CSV; secret columns
209        are sealed and shown masked until a reader opens them (audited)."""
210        resp = self._http.request(
211            "POST",
212            f"/admin/competitions/{competition_id}/messages/personalised",
213            json=self._personalised_body(
214                csv_text,
215                team_column=team_column,
216                subject=subject,
217                body=body,
218                secret_columns=secret_columns,
219                recipients=recipients,
220                email=email,
221                allow_reply=allow_reply,
222                overrides=overrides,
223                skip_lines=skip_lines,
224            ),
225        )
226        _raise_for_status(resp)
227        return ComposeResult.model_validate(resp.json())

Send each matched team its own message from the CSV; secret columns are sealed and shown masked until a reader opens them (audited).

def platform_threads( self, *, awaiting: str = '', unread: bool = False, limit: int = 50, offset: int = 0) -> ctfy.sdk._helpers.PagedList[ctfy.server.models.MessageThreadRow]:
229    def platform_threads(
230        self, *, awaiting: str = "", unread: bool = False, limit: int = 50, offset: int = 0
231    ) -> PagedList[MessageThreadRow]:
232        """Platform administrators: the platform's own threads."""
233        params: dict[str, Any] = {"limit": limit, "offset": offset}
234        if awaiting:
235            params["awaiting"] = awaiting
236        if unread:
237            params["unread"] = "true"
238        resp = self._http.request("GET", "/admin/messages/threads", params=params)
239        _raise_for_status(resp)
240        return _extract_items(resp.json(), MessageThreadRow)

Platform administrators: the platform's own threads.

def send_to_account( self, *, subject: str, body: str, user_id: str = '', email: str = '', send_email: bool = False, allow_reply: bool = True) -> ctfy.server.models.ComposeResult:
242    def send_to_account(
243        self,
244        *,
245        subject: str,
246        body: str,
247        user_id: str = "",
248        email: str = "",
249        send_email: bool = False,
250        allow_reply: bool = True,
251    ) -> ComposeResult:
252        """Platform administrators: write to one account, by id or address."""
253        resp = self._http.request(
254            "POST",
255            "/admin/messages",
256            json={
257                "user_id": user_id,
258                "email": email,
259                "subject": subject,
260                "body": body,
261                "send_email": send_email,
262                "allow_reply": allow_reply,
263            },
264        )
265        _raise_for_status(resp)
266        return ComposeResult.model_validate(resp.json())

Platform administrators: write to one account, by id or address.