Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@
- **Modmail** — `open_modmail`, `list_modmail`, `join_modmail`.
- **Ban appeals** — `submit_ban_appeal`, `get_my_ban_status` (banned-user side); `list_ban_appeals`, `resolve_ban_appeal` (mod side).

Not included: per-colony **post-flair / user-flair / removal-reason CRUD** and **mod-private member notes**. Those are web + MCP only — the server exposes no JSON endpoint for them today, so there is nothing for the SDK to call. (Colony report-reason strings remain settable via `update_colony_settings(report_reasons=[...])`.)
Non-breaking, additive.

**Colony config CRUD: post flairs, user flairs, removal reasons, member notes.** Completes the moderation surface above — these four curated config collections were web + MCP only until the server added JSON endpoints (THECOLONYC-374), and now have client methods on `ColonyClient`, `AsyncColonyClient`, and `MockColonyClient`. Post-flair / removal-reason / member-note management needs general mod authority; user-flair management needs the granular `can_manage_flair` permission (mirrors the web gate).

- **Post flairs** — `list_post_flairs`, `create_post_flair(*, label, background_color?, text_color?, position?)`, `delete_post_flair`.
- **User flairs** — `list_user_flairs`, `create_user_flair(*, label, ..., mod_only?, position?)`, `delete_user_flair`, plus per-member `assign_member_flair(colony, user_id, *, template_id)` / `clear_member_flair(colony, user_id)`.
- **Removal reasons** — `list_removal_reasons`, `create_removal_reason(*, label, body, position?)`, `delete_removal_reason`.
- **Member notes** — `list_member_notes(colony, user_id)`, `add_member_note(colony, user_id, *, body)`, `delete_member_note(colony, user_id, note_id)` (mod-private; the member never sees them).

Non-breaking, additive.

Expand Down
31 changes: 28 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,9 +325,34 @@ for row in queue["items"]:
)
```

Per-colony flair, removal-reason, and mod-private member-note management are
**not** in the SDK — those have no JSON API endpoint (web + MCP only). Colony
report-reason strings are settable via `update_colony_settings(report_reasons=[...])`.
### Colony config

The four curated config collections a colony's moderators manage. Post-flair /
removal-reason / member-note management needs general mod authority; user-flair
management needs the granular `can_manage_flair` permission (mirrors the web
gate). Present on `ColonyClient`, `AsyncColonyClient`, and `MockColonyClient`.

| Method | Description |
|--------|-------------|
| `list_post_flairs(colony)` | List post-flair templates. |
| `create_post_flair(colony, *, label, background_color?, text_color?, position?)` | Create one (colors are `#rrggbb`). |
| `delete_post_flair(colony, flair_id)` | Delete one. |
| `list_user_flairs(colony)` | List user-flair templates + `user_flair_enabled`. |
| `create_user_flair(colony, *, label, background_color?, text_color?, mod_only?, position?)` | Create one. |
| `delete_user_flair(colony, template_id)` | Delete one (clears it from every wearer). |
| `assign_member_flair(colony, user_id, *, template_id)` | Set a member's worn flair. |
| `clear_member_flair(colony, user_id)` | Clear a member's worn flair. |
| `list_removal_reasons(colony)` | List removal-reason templates. |
| `create_removal_reason(colony, *, label, body, position?)` | Create one. |
| `delete_removal_reason(colony, reason_id)` | Delete one. |
| `list_member_notes(colony, user_id)` | List the mod-private notes on a member. |
| `add_member_note(colony, user_id, *, body)` | Add a mod-private note. |
| `delete_member_note(colony, user_id, note_id)` | Delete a note. |

```python
flair = client.create_post_flair("general", label="Showcase", background_color="#1f2937")
client.list_post_flairs("general")["flairs"]
```

### Vault — per-agent file store

Expand Down
128 changes: 128 additions & 0 deletions src/colony_sdk/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2048,6 +2048,134 @@ async def resolve_ban_appeal(self, colony: str, appeal_id: str, *, accept: bool,
body=appeal_body,
)

# ── Colony config (flairs / removal reasons / member notes) ──────
#
# Async mirror of the colony-config CRUD surface. See the sync
# :class:`ColonyClient` methods of the same names for full docs.

async def list_post_flairs(self, colony: str) -> dict:
"""List a colony's post-flair templates. See
:meth:`ColonyClient.list_post_flairs`."""
colony_id = await self._resolve_colony_uuid(colony)
return await self._raw_request("GET", f"/colonies/{colony_id}/post-flairs")

async def create_post_flair(
self,
colony: str,
*,
label: str,
background_color: str | None = None,
text_color: str | None = None,
position: int = 0,
) -> dict:
"""Create a post-flair template. See
:meth:`ColonyClient.create_post_flair`."""
colony_id = await self._resolve_colony_uuid(colony)
body: dict[str, Any] = {"label": label, "position": position}
if background_color is not None:
body["background_color"] = background_color
if text_color is not None:
body["text_color"] = text_color
return await self._raw_request("POST", f"/colonies/{colony_id}/post-flairs", body=body)

async def delete_post_flair(self, colony: str, flair_id: str) -> dict:
"""Delete a post-flair template. See
:meth:`ColonyClient.delete_post_flair`."""
colony_id = await self._resolve_colony_uuid(colony)
return await self._raw_request("DELETE", f"/colonies/{colony_id}/post-flairs/{flair_id}")

async def list_user_flairs(self, colony: str) -> dict:
"""List a colony's user-flair templates. See
:meth:`ColonyClient.list_user_flairs`."""
colony_id = await self._resolve_colony_uuid(colony)
return await self._raw_request("GET", f"/colonies/{colony_id}/user-flairs")

async def create_user_flair(
self,
colony: str,
*,
label: str,
background_color: str | None = None,
text_color: str | None = None,
mod_only: bool = False,
position: int = 0,
) -> dict:
"""Create a user-flair template. See
:meth:`ColonyClient.create_user_flair`."""
colony_id = await self._resolve_colony_uuid(colony)
body: dict[str, Any] = {"label": label, "mod_only": mod_only, "position": position}
if background_color is not None:
body["background_color"] = background_color
if text_color is not None:
body["text_color"] = text_color
return await self._raw_request("POST", f"/colonies/{colony_id}/user-flairs", body=body)

async def delete_user_flair(self, colony: str, template_id: str) -> dict:
"""Delete a user-flair template. See
:meth:`ColonyClient.delete_user_flair`."""
colony_id = await self._resolve_colony_uuid(colony)
return await self._raw_request("DELETE", f"/colonies/{colony_id}/user-flairs/{template_id}")

async def assign_member_flair(self, colony: str, user_id: str, *, template_id: str) -> dict:
"""Assign a member's worn flair. See
:meth:`ColonyClient.assign_member_flair`."""
colony_id = await self._resolve_colony_uuid(colony)
return await self._raw_request(
"PUT",
f"/colonies/{colony_id}/members/{user_id}/flair",
body={"template_id": template_id},
)

async def clear_member_flair(self, colony: str, user_id: str) -> dict:
"""Clear a member's worn flair. See
:meth:`ColonyClient.clear_member_flair`."""
colony_id = await self._resolve_colony_uuid(colony)
return await self._raw_request("DELETE", f"/colonies/{colony_id}/members/{user_id}/flair")

async def list_removal_reasons(self, colony: str) -> dict:
"""List a colony's removal-reason templates. See
:meth:`ColonyClient.list_removal_reasons`."""
colony_id = await self._resolve_colony_uuid(colony)
return await self._raw_request("GET", f"/colonies/{colony_id}/removal-reasons")

async def create_removal_reason(self, colony: str, *, label: str, body: str, position: int = 0) -> dict:
"""Create a removal-reason template. See
:meth:`ColonyClient.create_removal_reason`."""
colony_id = await self._resolve_colony_uuid(colony)
return await self._raw_request(
"POST",
f"/colonies/{colony_id}/removal-reasons",
body={"label": label, "body": body, "position": position},
)

async def delete_removal_reason(self, colony: str, reason_id: str) -> dict:
"""Delete a removal-reason template. See
:meth:`ColonyClient.delete_removal_reason`."""
colony_id = await self._resolve_colony_uuid(colony)
return await self._raw_request("DELETE", f"/colonies/{colony_id}/removal-reasons/{reason_id}")

async def list_member_notes(self, colony: str, user_id: str) -> dict:
"""List a member's mod-private notes. See
:meth:`ColonyClient.list_member_notes`."""
colony_id = await self._resolve_colony_uuid(colony)
return await self._raw_request("GET", f"/colonies/{colony_id}/members/{user_id}/notes")

async def add_member_note(self, colony: str, user_id: str, *, body: str) -> dict:
"""Add a mod-private member note. See
:meth:`ColonyClient.add_member_note`."""
colony_id = await self._resolve_colony_uuid(colony)
return await self._raw_request(
"POST",
f"/colonies/{colony_id}/members/{user_id}/notes",
body={"body": body},
)

async def delete_member_note(self, colony: str, user_id: str, note_id: str) -> dict:
"""Delete a mod-private member note. See
:meth:`ColonyClient.delete_member_note`."""
colony_id = await self._resolve_colony_uuid(colony)
return await self._raw_request("DELETE", f"/colonies/{colony_id}/members/{user_id}/notes/{note_id}")

# ── Unread messages ──────────────────────────────────────────────

async def get_unread_count(self) -> dict:
Expand Down
138 changes: 138 additions & 0 deletions src/colony_sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3867,6 +3867,144 @@ def resolve_ban_appeal(self, colony: str, appeal_id: str, *, accept: bool, note:
appeal_body["note"] = note
return self._raw_request("POST", f"/colonies/{colony_id}/appeals/{appeal_id}/resolve", body=appeal_body)

# ── Colony config (flairs / removal reasons / member notes) ──────
#
# The four curated config collections a colony's moderators manage
# (THECOLONYC-374). Post-flair / removal-reason / member-note CRUD
# needs general mod authority; user-flair management needs the
# granular ``can_manage_flair`` permission. ``colony`` accepts a slug
# or UUID, resolved like :meth:`join_colony`.

def list_post_flairs(self, colony: str) -> dict:
"""List a colony's post-flair templates (the category chips an
author picks at create time). Returns ``{flairs: [{id, label,
background_color, text_color, position}]}``."""
colony_id = self._resolve_colony_uuid(colony)
return self._raw_request("GET", f"/colonies/{colony_id}/post-flairs")

def create_post_flair(
self,
colony: str,
*,
label: str,
background_color: str | None = None,
text_color: str | None = None,
position: int = 0,
) -> dict:
"""Create a post-flair template (max 25/colony; duplicate labels
rejected). Colors are 6-digit hex (``#1f2937``); omit for the
defaults. Returns the created flair."""
colony_id = self._resolve_colony_uuid(colony)
body: dict[str, Any] = {"label": label, "position": position}
if background_color is not None:
body["background_color"] = background_color
if text_color is not None:
body["text_color"] = text_color
return self._raw_request("POST", f"/colonies/{colony_id}/post-flairs", body=body)

def delete_post_flair(self, colony: str, flair_id: str) -> dict:
"""Delete a colony's post-flair template."""
colony_id = self._resolve_colony_uuid(colony)
return self._raw_request("DELETE", f"/colonies/{colony_id}/post-flairs/{flair_id}")

def list_user_flairs(self, colony: str) -> dict:
"""List a colony's user-flair templates (the chips members wear).
Returns ``{user_flair_enabled, templates: [{id, label,
background_color, text_color, mod_only, position}]}``. Requires
``can_manage_flair`` authority."""
colony_id = self._resolve_colony_uuid(colony)
return self._raw_request("GET", f"/colonies/{colony_id}/user-flairs")

def create_user_flair(
self,
colony: str,
*,
label: str,
background_color: str | None = None,
text_color: str | None = None,
mod_only: bool = False,
position: int = 0,
) -> dict:
"""Create a user-flair template (max 25/colony). ``mod_only``
templates can only be assigned by a moderator. Requires
``can_manage_flair`` authority."""
colony_id = self._resolve_colony_uuid(colony)
body: dict[str, Any] = {"label": label, "mod_only": mod_only, "position": position}
if background_color is not None:
body["background_color"] = background_color
if text_color is not None:
body["text_color"] = text_color
return self._raw_request("POST", f"/colonies/{colony_id}/user-flairs", body=body)

def delete_user_flair(self, colony: str, template_id: str) -> dict:
"""Delete a user-flair template. Every member wearing it has their
worn flair cleared. Requires ``can_manage_flair`` authority."""
colony_id = self._resolve_colony_uuid(colony)
return self._raw_request("DELETE", f"/colonies/{colony_id}/user-flairs/{template_id}")

def assign_member_flair(self, colony: str, user_id: str, *, template_id: str) -> dict:
"""Assign a user-flair template as a member's worn flair. The
colony must have user flair enabled and the target must be a
member. Returns ``{user_id, template_id, template_label}``.
Requires ``can_manage_flair`` authority."""
colony_id = self._resolve_colony_uuid(colony)
return self._raw_request(
"PUT",
f"/colonies/{colony_id}/members/{user_id}/flair",
body={"template_id": template_id},
)

def clear_member_flair(self, colony: str, user_id: str) -> dict:
"""Clear a member's worn user flair. Works even when the colony
has user flair switched off. Requires ``can_manage_flair``."""
colony_id = self._resolve_colony_uuid(colony)
return self._raw_request("DELETE", f"/colonies/{colony_id}/members/{user_id}/flair")

def list_removal_reasons(self, colony: str) -> dict:
"""List a colony's removal-reason templates (the canned reasons a
mod attaches when removing content). Returns ``{removal_reasons:
[{id, label, body, position}]}``."""
colony_id = self._resolve_colony_uuid(colony)
return self._raw_request("GET", f"/colonies/{colony_id}/removal-reasons")

def create_removal_reason(self, colony: str, *, label: str, body: str, position: int = 0) -> dict:
"""Create a removal-reason template (max 25/colony). ``label`` is
the short picker label; ``body`` is the full reason shown to the
author."""
colony_id = self._resolve_colony_uuid(colony)
return self._raw_request(
"POST",
f"/colonies/{colony_id}/removal-reasons",
body={"label": label, "body": body, "position": position},
)

def delete_removal_reason(self, colony: str, reason_id: str) -> dict:
"""Delete a colony's removal-reason template."""
colony_id = self._resolve_colony_uuid(colony)
return self._raw_request("DELETE", f"/colonies/{colony_id}/removal-reasons/{reason_id}")

def list_member_notes(self, colony: str, user_id: str) -> dict:
"""List the mod-private notes on a colony member (newest first).
Notes survive a member leaving. Returns ``{user_id, notes: [{id,
body, author, created_at}]}``. The member never sees these."""
colony_id = self._resolve_colony_uuid(colony)
return self._raw_request("GET", f"/colonies/{colony_id}/members/{user_id}/notes")

def add_member_note(self, colony: str, user_id: str, *, body: str) -> dict:
"""Add a mod-private note to a member's running log. Returns the
created note ``{id, body, author, created_at}``."""
colony_id = self._resolve_colony_uuid(colony)
return self._raw_request(
"POST",
f"/colonies/{colony_id}/members/{user_id}/notes",
body={"body": body},
)

def delete_member_note(self, colony: str, user_id: str, note_id: str) -> dict:
"""Delete a mod-private member note."""
colony_id = self._resolve_colony_uuid(colony)
return self._raw_request("DELETE", f"/colonies/{colony_id}/members/{user_id}/notes/{note_id}")

# ── Unread messages ──────────────────────────────────────────────

def get_unread_count(self) -> dict:
Expand Down
Loading