Changeset 0.28.0.1 (#173)
This commit is contained in:
711
docs/ICD/channels.md
Normal file
711
docs/ICD/channels.md
Normal file
@@ -0,0 +1,711 @@
|
||||
# Channels & Conversations
|
||||
|
||||
The **channel** is the universal conversation container. Messages,
|
||||
tool activity, attachments, and KB bindings all hang off a channel.
|
||||
Channels support multiple participants — users, personas, and
|
||||
sessions — making them the foundation for collaborative and
|
||||
multi-model workflows.
|
||||
|
||||
### Channel CRUD
|
||||
|
||||
**List channels** — paginated, sorted by last activity.
|
||||
|
||||
```
|
||||
GET /channels?page=1&per_page=50
|
||||
```
|
||||
|
||||
Returns pagination envelope. Each channel:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"title": "My Chat",
|
||||
"type": "direct|dm|group|channel|workflow|service",
|
||||
"description": "",
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"provider_config_id": "uuid|null",
|
||||
"system_prompt": "",
|
||||
"project_id": "uuid|null",
|
||||
"folder": "string|null",
|
||||
"tags": ["tag1", "tag2"],
|
||||
"participant_count": 1,
|
||||
"created_at": "...",
|
||||
"updated_at": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**Channel types:**
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `direct` | Single-user chat (default, backward compatible) |
|
||||
| `dm` | Human-to-human direct message, AI silent unless @mentioned |
|
||||
| `group` | Multi-user/multi-model collaborative channel |
|
||||
| `channel` | Named persistent space, configurable ai_mode |
|
||||
| `workflow` | Staged channel driven by workflow definitions |
|
||||
| `service` | Autonomous task execution, no human participant |
|
||||
|
||||
**Create channel:**
|
||||
|
||||
```
|
||||
POST /channels
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "New Chat",
|
||||
"type": "direct",
|
||||
"description": "",
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"provider_config_id": "uuid",
|
||||
"system_prompt": "",
|
||||
"folder": null,
|
||||
"tags": []
|
||||
}
|
||||
```
|
||||
|
||||
On create, the authenticated user is automatically added as a
|
||||
participant with `role: "owner"`. If `model` and `provider_config_id`
|
||||
are provided, the channel model roster (`channel_models`) is
|
||||
auto-populated with an initial entry.
|
||||
|
||||
**Get channel:**
|
||||
|
||||
```
|
||||
GET /channels/:id
|
||||
```
|
||||
|
||||
Returns single channel object.
|
||||
|
||||
**Update channel:**
|
||||
|
||||
```
|
||||
PUT /channels/:id
|
||||
```
|
||||
|
||||
Accepts partial updates — only fields present in the body are changed.
|
||||
Same field set as create.
|
||||
|
||||
**Delete channel:**
|
||||
|
||||
```
|
||||
DELETE /channels/:id
|
||||
```
|
||||
|
||||
Cascades: deletes messages, attachments, channel_models, channel_kbs.
|
||||
|
||||
### Message Tree
|
||||
|
||||
Switchboard uses a **tree** model for messages, not a flat list. Each
|
||||
message has a `parent_id` (NULL for root). Editing creates a sibling
|
||||
branch; regeneration creates an alternative sibling. The **cursor**
|
||||
tracks which child is "active" at each branch point.
|
||||
|
||||
**Get active path** — the primary endpoint for loading chat history:
|
||||
|
||||
```
|
||||
GET /channels/:id/path
|
||||
```
|
||||
|
||||
Returns the messages along the active branch from root to leaf,
|
||||
following the cursor at each fork. This is what the UI renders.
|
||||
|
||||
```json
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"channel_id": "uuid",
|
||||
"parent_id": "uuid|null",
|
||||
"role": "user|assistant|system|tool",
|
||||
"content": "...",
|
||||
"model": "claude-sonnet-4-20250514|null",
|
||||
"provider_config_id": "uuid|null",
|
||||
"participant_id": "uuid|null",
|
||||
"participant_type": "user|persona|session|null",
|
||||
"thinking": "...|null",
|
||||
"tool_calls": [...],
|
||||
"tool_results": [...],
|
||||
"attachments": [...],
|
||||
"has_siblings": true,
|
||||
"sibling_index": 0,
|
||||
"sibling_count": 2,
|
||||
"created_at": "..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`participant_id` and `participant_type` identify who authored the
|
||||
message. For `user` messages, this is the authenticated user. For
|
||||
`assistant` messages, this is the persona (if set) or null for raw
|
||||
model responses. Existing messages without participants return null.
|
||||
|
||||
**List all messages** — includes all branches, flat:
|
||||
|
||||
```
|
||||
GET /channels/:id/messages
|
||||
```
|
||||
|
||||
Legacy/debug endpoint. Returns every message in the channel regardless
|
||||
of branch. Not used by the standard frontend.
|
||||
|
||||
**Create message:**
|
||||
|
||||
```
|
||||
POST /channels/:id/messages
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Hello",
|
||||
"parent_id": "uuid|null",
|
||||
"attachments": ["attachment-uuid-1"]
|
||||
}
|
||||
```
|
||||
|
||||
**Edit message** — creates a sibling at the same level:
|
||||
|
||||
```
|
||||
POST /channels/:id/messages/:msgId/edit
|
||||
```
|
||||
|
||||
```json
|
||||
{ "content": "Revised question" }
|
||||
```
|
||||
|
||||
Creates a new message with the same `parent_id` as the original.
|
||||
Automatically updates the cursor to point to the new sibling.
|
||||
|
||||
**Regenerate** — creates an alternative assistant response:
|
||||
|
||||
```
|
||||
POST /channels/:id/messages/:msgId/regenerate
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"provider_config_id": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
Creates a new empty assistant message as a sibling of `msgId`,
|
||||
then streams the completion into it. The cursor is updated.
|
||||
|
||||
**Update cursor** — switch to a different branch:
|
||||
|
||||
```
|
||||
PUT /channels/:id/cursor
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"message_id": "uuid",
|
||||
"child_id": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
Sets which child is active at the `message_id` branch point.
|
||||
Subsequent `GET /channels/:id/path` will follow the new branch.
|
||||
|
||||
**List siblings** — all children of a message's parent:
|
||||
|
||||
```
|
||||
GET /channels/:id/messages/:msgId/siblings
|
||||
```
|
||||
|
||||
Returns `{ "siblings": [...] }` with abbreviated message objects
|
||||
(id, role, content preview, created_at).
|
||||
|
||||
### Completions & Streaming
|
||||
|
||||
The single most complex endpoint. Sends a message to an LLM provider
|
||||
and streams the response via Server-Sent Events.
|
||||
|
||||
```
|
||||
POST /chat/completions
|
||||
```
|
||||
|
||||
**Request:**
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_id": "uuid",
|
||||
"message": "User's message text",
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"provider_config_id": "uuid",
|
||||
"persona_id": "uuid|null",
|
||||
"parent_id": "uuid|null",
|
||||
"tools_enabled": true,
|
||||
"tool_ids": ["web_search", "calculator"],
|
||||
"attachments": ["attachment-uuid"],
|
||||
"extra_body": {}
|
||||
}
|
||||
```
|
||||
|
||||
`channel_id` is required.
|
||||
|
||||
`extra_body` is a freeform JSON object merged into the provider request
|
||||
(provider-specific parameters like `temperature`, `reasoning`, etc.).
|
||||
|
||||
**Participant scoping:** The requesting user must be a participant in
|
||||
the channel. Messages are attributed to the authenticated user (or
|
||||
session). In `group` channels, all participants see all messages in
|
||||
real time via WebSocket.
|
||||
|
||||
**Persona resolution:** The primary completion path. If `persona_id`
|
||||
is set (explicitly or via `@mention` resolution from message content),
|
||||
the handler loads the persona's system prompt, model, provider config,
|
||||
and KB bindings. Per-message `model`/`provider_config_id` override the
|
||||
persona's defaults. In multi-persona channels, `@mention` in the
|
||||
message content is parsed to resolve the target persona from the
|
||||
channel's participant list.
|
||||
|
||||
**Routing resolution:** After config resolution, the routing policy
|
||||
evaluator (`evaluateRouting()`) may redirect to a different provider
|
||||
based on active policies (see §10.4). The winning provider config's
|
||||
credentials are loaded and used for the actual API call.
|
||||
|
||||
**SSE Stream Format:**
|
||||
|
||||
The response is `Content-Type: text/event-stream`. Events:
|
||||
|
||||
```
|
||||
data: {"content":"Hello"}
|
||||
|
||||
data: {"content":" world"}
|
||||
|
||||
data: {"reasoning":"Let me think..."}
|
||||
|
||||
event: tool_use
|
||||
data: {"id":"call_1","name":"web_search","input":{"query":"..."}}
|
||||
|
||||
event: tool_result
|
||||
data: {"id":"call_1","content":"Search results..."}
|
||||
|
||||
data: {"content":"Based on the search..."}
|
||||
|
||||
event: finish
|
||||
data: {"message_id":"uuid","model":"claude-sonnet-4-20250514","usage":{"input_tokens":150,"output_tokens":42}}
|
||||
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
| Event | `event:` field | Payload |
|
||||
|-------|---------------|---------|
|
||||
| Content delta | _(unnamed)_ | `{ "content": "text" }` |
|
||||
| Reasoning delta | _(unnamed)_ | `{ "reasoning": "text" }` |
|
||||
| Tool invocation | `tool_use` | `{ "id", "name", "input" }` |
|
||||
| Tool result | `tool_result` | `{ "id", "content" }` |
|
||||
| Stream complete | `finish` | `{ "message_id", "model", "usage" }` |
|
||||
| End sentinel | _(unnamed)_ | `[DONE]` (literal string) |
|
||||
| Error mid-stream | `error` | `{ "error": "message" }` |
|
||||
|
||||
**Tool cycle:** Tool use and tool result events can repeat up to
|
||||
`max_tool_iterations` (configured server-side). The handler executes
|
||||
tools, feeds results back to the model, and continues streaming.
|
||||
|
||||
**Response headers:**
|
||||
|
||||
```
|
||||
X-Switchboard-Provider: providerID/configID
|
||||
X-Switchboard-Route: policy-name (when routing policy is active)
|
||||
```
|
||||
|
||||
**List available tools:**
|
||||
|
||||
```
|
||||
GET /tools
|
||||
```
|
||||
|
||||
Returns `{ "tools": [...] }` where each tool has `name`, `description`,
|
||||
`parameters` (JSON Schema), and `category`.
|
||||
|
||||
### Multi-Model Roster (Raw Access)
|
||||
|
||||
The primary way to add AI to a channel is by adding a **persona as a
|
||||
participant** (§3.7). Adding a persona automatically populates the
|
||||
channel model roster with the persona's underlying model.
|
||||
|
||||
For the rare case where a user needs raw model access without a
|
||||
persona wrapper, the `channel_models` table provides direct model
|
||||
roster management. This is the 1% escape hatch — most users should
|
||||
never need it.
|
||||
|
||||
```
|
||||
GET /channels/:id/models → { "models": [...] }
|
||||
POST /channels/:id/models ← { "model": "...", "provider_config_id": "...", "display_name": "..." }
|
||||
PATCH /channels/:id/models/:modelId ← { "display_name": "..." }
|
||||
DELETE /channels/:id/models/:modelId
|
||||
```
|
||||
|
||||
Each roster entry:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"channel_id": "uuid",
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"provider_config_id": "uuid",
|
||||
"display_name": "Claude",
|
||||
"is_default": true,
|
||||
"persona_id": "uuid|null",
|
||||
"created_at": "..."
|
||||
}
|
||||
```
|
||||
|
||||
`persona_id`: set when this roster entry was auto-created by adding a
|
||||
persona participant. Null for manually-added raw models.
|
||||
|
||||
**`@mention` resolution:** When a user types `@` in a channel, the
|
||||
autocomplete shows persona participants first (by display name), then
|
||||
raw model roster entries. The target of a `@mention` is always
|
||||
resolved to a specific `provider_config_id` + `model` pair — never
|
||||
an ambiguous bare model name.
|
||||
|
||||
### Channel Utilities
|
||||
|
||||
**Summarize channel** (compaction):
|
||||
|
||||
```
|
||||
POST /channels/:id/summarize
|
||||
```
|
||||
|
||||
Triggers conversation summarization via the utility model role.
|
||||
Returns `{ "message": "summarized", "summary_id": "uuid" }`.
|
||||
|
||||
**Generate title:**
|
||||
|
||||
```
|
||||
POST /channels/:id/generate-title
|
||||
```
|
||||
|
||||
Uses the utility model to auto-generate a title from the first
|
||||
exchange. Returns `{ "title": "Generated Title" }`.
|
||||
|
||||
### Files
|
||||
|
||||
All binary content associated with channels — user uploads, tool-
|
||||
generated artifacts, system files — lives in the unified `files`
|
||||
table backed by the ObjectStore (see §X). The old `attachments`
|
||||
table and routes are removed.
|
||||
|
||||
**Upload (user):**
|
||||
|
||||
```
|
||||
POST /channels/:id/files
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
Field: `file`. Sets `origin: "user_upload"`. Returns the file object.
|
||||
|
||||
**Create (tool output):**
|
||||
|
||||
```
|
||||
POST /files
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"channel_id": "uuid",
|
||||
"message_id": "uuid",
|
||||
"filename": "generated_image.png",
|
||||
"content_type": "image/png",
|
||||
"display_hint": "inline",
|
||||
"metadata": { "tool_name": "image_generation" }
|
||||
}
|
||||
```
|
||||
|
||||
Body: multipart `file` field or base64 `data` field.
|
||||
Sets `origin: "tool_output"`. Returns file object.
|
||||
|
||||
**List by channel:**
|
||||
|
||||
```
|
||||
GET /channels/:id/files?origin=user_upload
|
||||
```
|
||||
|
||||
Returns `{ "files": [...] }`. Filterable by `origin`, `content_type`.
|
||||
|
||||
**List by message:**
|
||||
|
||||
```
|
||||
GET /messages/:id/files → { "files": [...] }
|
||||
```
|
||||
|
||||
How the frontend discovers generated artifacts to render inline.
|
||||
|
||||
**List by user (file manager):**
|
||||
|
||||
```
|
||||
GET /files?page=1&per_page=50 → { "files": [...], "total": N }
|
||||
```
|
||||
|
||||
All files owned by the authenticated user, paginated.
|
||||
|
||||
**Get metadata:**
|
||||
|
||||
```
|
||||
GET /files/:id
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"channel_id": "uuid",
|
||||
"message_id": "uuid|null",
|
||||
"user_id": "uuid",
|
||||
"origin": "user_upload|tool_output|system",
|
||||
"filename": "report.pdf",
|
||||
"content_type": "application/pdf",
|
||||
"size_bytes": 1048576,
|
||||
"display_hint": "inline|download|thumbnail",
|
||||
"extracted_text": "...|null",
|
||||
"metadata": {},
|
||||
"created_at": "...",
|
||||
"updated_at": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**Download:**
|
||||
|
||||
```
|
||||
GET /files/:id/download
|
||||
```
|
||||
|
||||
Returns the file with appropriate `Content-Type` and
|
||||
`Content-Disposition` headers.
|
||||
|
||||
**Thumbnail:**
|
||||
|
||||
```
|
||||
GET /files/:id/thumbnail
|
||||
```
|
||||
|
||||
**Delete:**
|
||||
|
||||
```
|
||||
DELETE /files/:id
|
||||
```
|
||||
|
||||
Deletes metadata + blob. Channel deletion cascades via FK +
|
||||
`DeletePrefix`.
|
||||
|
||||
### Channel Participants
|
||||
|
||||
The participant system is how users and AI interact in a channel.
|
||||
**Personas are the primary way to add AI** — adding a persona as a
|
||||
participant brings its full identity (name, avatar, system prompt,
|
||||
model, provider config, KB bindings) into the channel. This is the
|
||||
99% path. Raw model access without a persona is available via the
|
||||
model roster (§3.4) for power users.
|
||||
|
||||
Every channel has at least one participant (the owner). `direct`
|
||||
channels have exactly one user participant and optionally one or more
|
||||
persona participants. `group` and `workflow` channels support multiple
|
||||
participants of mixed types.
|
||||
|
||||
**List participants:**
|
||||
|
||||
```
|
||||
GET /channels/:id/participants
|
||||
```
|
||||
|
||||
Returns `{ "participants": [...] }`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"channel_id": "uuid",
|
||||
"participant_type": "user|persona|session",
|
||||
"participant_id": "uuid",
|
||||
"role": "owner|member|observer",
|
||||
"display_name": "Jane Doe",
|
||||
"avatar_url": "...|null",
|
||||
"joined_at": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**Participant types:**
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `user` | Authenticated user. `participant_id` = `users.id` |
|
||||
| `persona` | AI persona added to channel. `participant_id` = `personas.id`. Brings model, system prompt, KB bindings |
|
||||
| `session` | Anonymous/session participant (workflow intake). `participant_id` = session token |
|
||||
|
||||
**Participant roles:**
|
||||
|
||||
| Role | Capabilities |
|
||||
|------|-------------|
|
||||
| `owner` | Full control: add/remove participants, delete channel, all member capabilities |
|
||||
| `member` | Send messages, trigger completions, view history |
|
||||
| `observer` | Read-only: view messages, no send |
|
||||
|
||||
**Add participant:**
|
||||
|
||||
```
|
||||
POST /channels/:id/participants
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"participant_type": "user|persona",
|
||||
"participant_id": "uuid",
|
||||
"role": "member"
|
||||
}
|
||||
```
|
||||
|
||||
Requires `owner` role in the channel. Adding a `persona` participant:
|
||||
- Makes the persona available as an `@mention` target
|
||||
- Adds the persona's model to the channel model roster automatically
|
||||
- Scopes the persona's KB bindings into the channel's KB resolution chain
|
||||
- The persona's avatar and display name appear in the participant list
|
||||
|
||||
**Update participant role:**
|
||||
|
||||
```
|
||||
PATCH /channels/:id/participants/:participantId
|
||||
```
|
||||
|
||||
```json
|
||||
{ "role": "observer" }
|
||||
```
|
||||
|
||||
**Remove participant:**
|
||||
|
||||
```
|
||||
DELETE /channels/:id/participants/:participantId
|
||||
```
|
||||
|
||||
Cannot remove the last owner. Removing a `persona` participant also
|
||||
removes its auto-created model roster entry.
|
||||
|
||||
**Completion routing:** When `@PersonaName` appears in message
|
||||
content, the completion handler resolves to that persona's model,
|
||||
provider config, and system prompt. When no `@mention` is present in
|
||||
a multi-persona channel, the channel's default persona (first added)
|
||||
handles the response. The `persona_id` field in the completion
|
||||
request (§3.3) can also be set explicitly to override `@mention`
|
||||
resolution.
|
||||
|
||||
**Backward compatibility:** Existing `direct` channels that predate
|
||||
the participant system are auto-migrated on first access — a single
|
||||
`user` participant with `role: "owner"` is created from the channel's
|
||||
legacy `user_id` column.
|
||||
|
||||
### Presence
|
||||
|
||||
Heartbeat-based online status. Not channel-scoped — tracks global user presence.
|
||||
|
||||
**Heartbeat:**
|
||||
|
||||
```
|
||||
POST /presence/heartbeat
|
||||
```
|
||||
|
||||
Client sends every 30 seconds. Server upserts `user_presence` row.
|
||||
No request body needed. Returns `{ "ok": true }`.
|
||||
|
||||
**Auth:** Authenticated.
|
||||
|
||||
**Bulk query:**
|
||||
|
||||
```
|
||||
GET /presence?users=alice,bob,charlie
|
||||
```
|
||||
|
||||
Returns online status for listed usernames. Threshold: online if
|
||||
last heartbeat < 90 seconds ago.
|
||||
|
||||
```json
|
||||
{
|
||||
"users": {
|
||||
"alice": { "status": "online", "last_seen": "..." },
|
||||
"bob": { "status": "offline", "last_seen": "..." }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Subsequent updates delivered via WebSocket `presence.changed` events.
|
||||
|
||||
**Typing indicators** are sent via WebSocket (see websocket.md). The
|
||||
typing event payload includes `participant_id` and `participant_type`.
|
||||
|
||||
### Chat Folders
|
||||
|
||||
User-scoped grouping for chats. Folders have no semantic weight — they
|
||||
are named drawers.
|
||||
|
||||
```
|
||||
GET /folders → list user's folders
|
||||
POST /folders ← { "name": "Research" }
|
||||
PUT /folders/:id ← { "name": "Renamed" }
|
||||
DELETE /folders/:id → chats become unfiled
|
||||
```
|
||||
|
||||
**Folder object:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"user_id": "uuid",
|
||||
"name": "Research",
|
||||
"parent_id": "uuid|null",
|
||||
"sort_order": 0,
|
||||
"created_at": "...",
|
||||
"updated_at": "..."
|
||||
}
|
||||
```
|
||||
|
||||
Moving a chat into/out of a folder is done via `PUT /channels/:id`
|
||||
with `{ "folder_id": "uuid" }` or `{ "folder_id": null }`.
|
||||
|
||||
**Auth:** Authenticated (user-scoped).
|
||||
|
||||
### Channel Configuration
|
||||
|
||||
Additional channel fields (set via `PUT /channels/:id`):
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `ai_mode` | string | `auto` | `auto`, `mention_only`, or `off` |
|
||||
| `topic` | string | null | Short description shown in header |
|
||||
| `allow_anonymous` | boolean | false | Session participants can join (workflow channels) |
|
||||
| `kb_auto_inject` | boolean | false | Top-K KB chunks auto-prepend to context |
|
||||
|
||||
`ai_mode` behavioral matrix:
|
||||
|
||||
| Channel type | Default ai_mode | Notes |
|
||||
|-------------|----------------|-------|
|
||||
| `direct` | `auto` | Existing behavior |
|
||||
| `dm` | `mention_only` | AI silent unless @mentioned |
|
||||
| `group` | `auto` | Configurable per channel |
|
||||
| `channel` | `auto` | Configurable per channel |
|
||||
|
||||
### Mark Read
|
||||
|
||||
```
|
||||
POST /channels/:id/mark-read
|
||||
```
|
||||
|
||||
Updates the user's `last_read_at` cursor. Used for unread count
|
||||
calculation.
|
||||
|
||||
**Auth:** Authenticated.
|
||||
|
||||
### User Search
|
||||
|
||||
```
|
||||
GET /users/search?q=alice
|
||||
```
|
||||
|
||||
Returns `{ "data": [{ "id", "username", "handle", "display_name", "avatar_url" }] }`.
|
||||
Used for DM creation and participant picker. Matches on handle.
|
||||
|
||||
**Auth:** Authenticated.
|
||||
|
||||
|
||||
---
|
||||
Reference in New Issue
Block a user