docs: add share bot API integration guide

This commit is contained in:
2026-08-16 01:25:37 +07:00 Unverified
parent ccaaf798ff
commit 883e147da4
+214
View File
@@ -0,0 +1,214 @@
# Share API bot integration
Use this endpoint when a trusted Discord bot needs to publish a member's text
attachment and return a public Erika Share link.
```http
POST /api/share
Authorization: Bearer <SHARE_BOT_SECRET>
Content-Type: multipart/form-data
```
The endpoint does not use a NextAuth session or a public API key. It is private and
requires the shared bearer secret configured on the website as `SHARE_BOT_SECRET`.
Store the same secret in the bot's private environment; never place it in source
code, Discord messages, browser code, or logs.
## Required website configuration
The website needs these values before the endpoint can work:
- `SHARE_BOT_SECRET`: long random secret shared only with the bot.
- `BASE_URL`, or `NEXTAUTH_URL` as its fallback: canonical website origin used in
returned links.
- Discord bot token and guild ID already used by Erika's Discord integration.
- Redis configuration for rate limiting.
The bot can use its own variable names:
```dotenv
ERIKA_BASE_URL=https://example.com
ERIKA_SHARE_BOT_SECRET=replace-with-the-same-secret
```
Do not include a trailing slash in `ERIKA_BASE_URL`.
## Multipart fields
| Field | Required | Contract |
| --- | --- | --- |
| `file` | Yes | Non-empty UTF-8 `.txt` file, maximum 5 MiB. Blank text and NUL characters are rejected. |
| `senderDiscordId` | Yes | `message.author.id`, as a 15–22 digit Discord snowflake. Do not send the bot's ID. |
| `description` | No | Plain text, maximum 5,000 characters. |
| `image` | No | PNG, JPEG, WebP, or GIF, maximum 50 MiB. The declared MIME type must match the file contents. |
Do not send `displayName`, `avatarUrl`, `authorName`, or `authorAvatarUrl`. The API
rejects those fields. It verifies that `senderDiscordId` belongs to the configured
Discord guild and fetches the member's global display name and global avatar directly
from Discord.
## Discord.js example
This example uses the built-in `fetch`, `FormData`, and `Blob` APIs available in
modern Node.js runtimes. It publishes the first `.txt` attachment on a message and
uses the message text as the optional description.
```ts
import type { Attachment, Message } from "discord.js";
type ShareSuccess = {
success: true;
share: {
id: string;
url: string;
author: {
displayName: string;
avatarUrl: string | null;
};
imageUrl: string | null;
createdAt: string;
};
};
type ShareFailure = {
success: false;
error: string;
};
async function attachmentBlob(attachment: Attachment) {
const response = await fetch(attachment.url);
if (!response.ok) {
throw new Error(`Unable to download ${attachment.name}: ${response.status}`);
}
return new Blob([await response.arrayBuffer()], {
type: attachment.contentType ?? "application/octet-stream",
});
}
export async function publishTextAttachment(message: Message) {
if (message.author.bot) return;
const textAttachment = message.attachments.find((attachment) =>
attachment.name?.toLowerCase().endsWith(".txt"),
);
if (!textAttachment?.name) return;
const baseUrl = process.env.ERIKA_BASE_URL;
const secret = process.env.ERIKA_SHARE_BOT_SECRET;
if (!baseUrl || !secret) {
throw new Error("Erika Share API is not configured");
}
const form = new FormData();
form.set("file", await attachmentBlob(textAttachment), textAttachment.name);
form.set("senderDiscordId", message.author.id);
const description = message.content.trim();
if (description) form.set("description", description.slice(0, 5_000));
const response = await fetch(`${baseUrl}/api/share`, {
method: "POST",
headers: {
Authorization: `Bearer ${secret}`,
},
body: form,
});
const result = (await response.json()) as ShareSuccess | ShareFailure;
if (!response.ok || !result.success) {
const retryAfter = response.headers.get("retry-after");
const retryNote = retryAfter ? `; retry after ${retryAfter}s` : "";
throw new Error(`Erika Share failed (${response.status}): ${result.error}${retryNote}`);
}
await message.reply(result.share.url);
return result.share;
}
```
Do not manually set the `Content-Type` request header. `FormData` supplies the
required multipart boundary automatically.
To include the first supported image attachment, add it before the request:
```ts
const imageAttachment = message.attachments.find((attachment) =>
attachment.contentType?.startsWith("image/"),
);
if (imageAttachment?.name) {
form.set("image", await attachmentBlob(imageAttachment), imageAttachment.name);
}
```
## cURL example
```bash
curl --fail-with-body \
-X POST "https://example.com/api/share" \
-H "Authorization: Bearer $ERIKA_SHARE_BOT_SECRET" \
-F "file=@./message.txt;type=text/plain" \
-F "senderDiscordId=123456789012345678" \
-F "description=Optional context for this share"
```
## Successful response
The API returns HTTP `201`:
```json
{
"success": true,
"share": {
"id": "a1b2c3d4",
"url": "https://example.com/share/a1b2c3d4",
"author": {
"displayName": "Example User",
"avatarUrl": "https://cdn.discordapp.com/avatars/..."
},
"imageUrl": null,
"createdAt": "2026-08-16T00:00:00.000Z"
}
}
```
The bot should post `share.url`. The shared page is public and does not require the
recipient to log in.
## Error response
Failures use this shape:
```json
{
"success": false,
"error": "Human-readable error"
}
```
| Status | Meaning |
| --- | --- |
| `400` | Missing/malformed multipart data, invalid sender ID, blank text, invalid UTF-8, NUL content, or a supplied profile field. |
| `401` | Missing or incorrect bearer secret. |
| `413` | Text, description, or image exceeds its limit. |
| `415` | The main file is not `.txt`, or the image type/content is unsupported. |
| `422` | The sender is not a member of the configured Discord guild. |
| `429` | A rate limit was exceeded. Read the `Retry-After` response header. |
| `503` | Bot API configuration, Discord profile lookup, or Redis rate limiting is unavailable. |
| `500` | Unexpected internal failure. Log the status and error without logging the bearer secret. |
## Rate limits
- 60 authenticated bot API requests per hour globally.
- 10 shares per hour per Discord sender, combined across bot and website uploads.
On HTTP `429`, wait for the number of seconds in `Retry-After` before retrying. Do
not retry `400`, `401`, `413`, `415`, or `422` automatically. A short bounded retry
with backoff is appropriate for `503` and transient `500` responses.
## Identity and duplicate safety
The website creates or updates the member's linked NextAuth user and Discord account
inside a transaction. Repeated or concurrent uploads for one Discord ID resolve to
the same user/account pair. If that member later signs in through Discord OAuth, the
login reuses the bot-created user instead of creating a duplicate.