diff --git a/Makefile b/Makefile index 5f3893c..8bc67c5 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: check check-rules test-api up down smoke render-config package-plugin discord-control-up smoke-discord-control +.PHONY: check check-rules test-api up down smoke render-config package-plugin check: cd plugin && npm run check @@ -24,8 +24,3 @@ render-config: package-plugin: node scripts/package-plugin.mjs -discord-control-up: - cd discord-control-api && node server.mjs - -smoke-discord-control: - ./scripts/smoke-discord-control.sh diff --git a/README.md b/README.md index 24c3fdc..dfcf810 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Dirigent adds deterministic logic **before model selection** and **turn-based sp - `plugin/` — OpenClaw plugin (gate + turn manager + moderator presence) - `no-reply-api/` — OpenAI-compatible API that always returns `NO_REPLY` -- `discord-control-api/` — Discord admin extension API (private channels + member list) +- Discord admin actions are now handled in-plugin via direct Discord REST API calls (no sidecar service) - `docs/` — rollout, integration, run-mode notes, turn-wakeup analysis - `scripts/` — smoke/dev/helper checks - `Makefile` — common dev commands (`make check`, `make check-rules`, `make test-api`, `make smoke-discord-control`, `make up`) diff --git a/discord-control-api/package.json b/discord-control-api/package.json deleted file mode 100644 index ef1f8c6..0000000 --- a/discord-control-api/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "dirigent-discord-control-api", - "version": "0.1.0", - "private": true, - "type": "module", - "scripts": { - "start": "node server.mjs" - } -} diff --git a/discord-control-api/server.mjs b/discord-control-api/server.mjs deleted file mode 100644 index 27a21db..0000000 --- a/discord-control-api/server.mjs +++ /dev/null @@ -1,388 +0,0 @@ -import http from "node:http"; - -const port = Number(process.env.PORT || 8790); -const authToken = process.env.AUTH_TOKEN || ""; -const requireAuthToken = String(process.env.REQUIRE_AUTH_TOKEN || "false").toLowerCase() === "true"; -const discordToken = process.env.DISCORD_BOT_TOKEN || ""; -const discordBase = "https://discord.com/api/v10"; - -const enabledActions = { - channelPrivateCreate: String(process.env.ENABLE_CHANNEL_PRIVATE_CREATE || "true").toLowerCase() !== "false", - channelPrivateUpdate: String(process.env.ENABLE_CHANNEL_PRIVATE_UPDATE || "true").toLowerCase() !== "false", - memberList: String(process.env.ENABLE_MEMBER_LIST || "true").toLowerCase() !== "false", -}; - -const allowedGuildIds = new Set( - String(process.env.ALLOWED_GUILD_IDS || "") - .split(",") - .map((s) => s.trim()) - .filter(Boolean), -); - -const allowedCallerIds = new Set( - String(process.env.ALLOWED_CALLER_IDS || "") - .split(",") - .map((s) => s.trim()) - .filter(Boolean), -); - -const BIT_VIEW_CHANNEL = 1024n; -const BIT_SEND_MESSAGES = 2048n; -const BIT_READ_MESSAGE_HISTORY = 65536n; - -const MAX_MEMBER_FIELDS = Math.max(1, Number(process.env.MAX_MEMBER_FIELDS || 20)); -const MAX_MEMBER_RESPONSE_BYTES = Math.max(2048, Number(process.env.MAX_MEMBER_RESPONSE_BYTES || 500000)); -const MAX_PRIVATE_MUTATION_TARGETS = Math.max(1, Number(process.env.MAX_PRIVATE_MUTATION_TARGETS || 200)); - -function sendJson(res, status, payload) { - res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" }); - res.end(JSON.stringify(payload)); -} - -function fail(status, code, message, details) { - return { status, code, message, details }; -} - -function readCallerId(req) { - const v = req.headers["x-openclaw-caller-id"] || req.headers["x-caller-id"]; - return typeof v === "string" ? v.trim() : ""; -} - -function ensureControlAuth(req) { - if (requireAuthToken && !authToken) { - throw fail(500, "auth_misconfigured", "REQUIRE_AUTH_TOKEN=true but AUTH_TOKEN is empty"); - } - if (!authToken) return; - const header = req.headers.authorization || ""; - if (header !== `Bearer ${authToken}`) { - throw fail(401, "unauthorized", "invalid or missing bearer token"); - } - - if (allowedCallerIds.size > 0) { - const callerId = readCallerId(req); - if (!callerId || !allowedCallerIds.has(callerId)) { - throw fail(403, "caller_forbidden", "caller is not in ALLOWED_CALLER_IDS", { callerId: callerId || null }); - } - } -} - -function ensureDiscordToken() { - if (!discordToken) { - throw fail(500, "discord_token_missing", "missing DISCORD_BOT_TOKEN"); - } -} - -function ensureGuildAllowed(guildId) { - if (allowedGuildIds.size === 0) return; - if (!allowedGuildIds.has(guildId)) { - throw fail(403, "guild_forbidden", "guild is not in ALLOWED_GUILD_IDS", { guildId }); - } -} - -function ensureActionEnabled(action) { - if (action === "channel-private-create" && !enabledActions.channelPrivateCreate) { - throw fail(403, "action_disabled", "channel-private-create is disabled"); - } - if (action === "channel-private-update" && !enabledActions.channelPrivateUpdate) { - throw fail(403, "action_disabled", "channel-private-update is disabled"); - } - if (action === "member-list" && !enabledActions.memberList) { - throw fail(403, "action_disabled", "member-list is disabled"); - } -} - -async function discordRequest(path, init = {}) { - ensureDiscordToken(); - const headers = { - Authorization: `Bot ${discordToken}`, - "Content-Type": "application/json", - ...(init.headers || {}), - }; - - const r = await fetch(`${discordBase}${path}`, { ...init, headers }); - const text = await r.text(); - let data = text; - try { - data = text ? JSON.parse(text) : {}; - } catch {} - - if (!r.ok) { - throw fail(r.status, "discord_api_error", `discord api returned ${r.status}`, data); - } - return data; -} - -function toStringMask(v, fallback) { - if (v === undefined || v === null || v === "") return String(fallback); - if (typeof v === "string") return v; - if (typeof v === "number") return String(Math.floor(v)); - if (typeof v === "bigint") return String(v); - throw fail(400, "invalid_mask", "invalid permission bit mask"); -} - -function buildPrivateOverwrites({ guildId, allowedUserIds = [], allowedRoleIds = [], allowMask, denyEveryoneMask }) { - const allowDefault = BIT_VIEW_CHANNEL | BIT_SEND_MESSAGES | BIT_READ_MESSAGE_HISTORY; - const denyDefault = BIT_VIEW_CHANNEL; - - const everyoneDeny = toStringMask(denyEveryoneMask, denyDefault); - const targetAllow = toStringMask(allowMask, allowDefault); - - const overwrites = [ - { - id: guildId, - type: 0, - allow: "0", - deny: everyoneDeny, - }, - ]; - - for (const roleId of allowedRoleIds) { - overwrites.push({ id: roleId, type: 0, allow: targetAllow, deny: "0" }); - } - for (const userId of allowedUserIds) { - overwrites.push({ id: userId, type: 1, allow: targetAllow, deny: "0" }); - } - - return overwrites; -} - -function parseFieldList(input) { - if (Array.isArray(input)) return input.map((x) => String(x).trim()).filter(Boolean); - if (typeof input === "string") return input.split(",").map((x) => x.trim()).filter(Boolean); - return []; -} - -function normalizeIdList(value, label) { - const arr = Array.isArray(value) ? value.map(String).map((v) => v.trim()).filter(Boolean) : []; - if (arr.length > MAX_PRIVATE_MUTATION_TARGETS) { - throw fail(400, "bad_request", `${label} exceeds MAX_PRIVATE_MUTATION_TARGETS`, { - label, - limit: MAX_PRIVATE_MUTATION_TARGETS, - size: arr.length, - }); - } - return arr; -} - -function pick(obj, keys) { - if (!obj || typeof obj !== "object") return obj; - const out = {}; - for (const k of keys) { - if (k in obj) out[k] = obj[k]; - } - return out; -} - -function projectMember(member, fields) { - if (!fields.length) return member; - const base = pick(member, fields); - if (fields.some((f) => f.startsWith("user.")) && member?.user) { - const userFields = fields - .filter((f) => f.startsWith("user.")) - .map((f) => f.slice(5)) - .filter(Boolean); - base.user = pick(member.user, userFields); - } - return base; -} - -async function actionChannelPrivateCreate(body) { - const guildId = String(body.guildId || "").trim(); - const name = String(body.name || "").trim(); - if (!guildId) throw fail(400, "bad_request", "guildId is required"); - if (!name) throw fail(400, "bad_request", "name is required"); - ensureGuildAllowed(guildId); - - const payload = { - name, - type: Number.isInteger(body.type) ? body.type : 0, - parent_id: body.parentId || undefined, - topic: body.topic || undefined, - position: Number.isInteger(body.position) ? body.position : undefined, - nsfw: typeof body.nsfw === "boolean" ? body.nsfw : undefined, - permission_overwrites: buildPrivateOverwrites({ - guildId, - allowedUserIds: normalizeIdList(body.allowedUserIds, "allowedUserIds"), - allowedRoleIds: normalizeIdList(body.allowedRoleIds, "allowedRoleIds"), - allowMask: body.allowMask, - denyEveryoneMask: body.denyEveryoneMask, - }), - }; - - if (body.dryRun === true) { - return { ok: true, action: "channel-private-create", dryRun: true, payload }; - } - - const channel = await discordRequest(`/guilds/${guildId}/channels`, { - method: "POST", - body: JSON.stringify(payload), - }); - - return { ok: true, action: "channel-private-create", channel }; -} - -async function actionChannelPrivateUpdate(body) { - const guildId = String(body.guildId || "").trim(); - const channelId = String(body.channelId || "").trim(); - if (!guildId) throw fail(400, "bad_request", "guildId is required"); - if (!channelId) throw fail(400, "bad_request", "channelId is required"); - ensureGuildAllowed(guildId); - - const allowMask = toStringMask(body.allowMask, BIT_VIEW_CHANNEL | BIT_SEND_MESSAGES | BIT_READ_MESSAGE_HISTORY); - const denyMask = toStringMask(body.denyMask, 0n); - const mode = String(body.mode || "merge").trim(); - if (mode !== "merge" && mode !== "replace") { - throw fail(400, "bad_request", "mode must be merge or replace", { mode }); - } - - const addUserIds = normalizeIdList(body.addUserIds, "addUserIds"); - const addRoleIds = normalizeIdList(body.addRoleIds, "addRoleIds"); - const removeTargetIds = normalizeIdList(body.removeTargetIds, "removeTargetIds"); - - const existing = await discordRequest(`/channels/${channelId}`); - const existingOverwrites = Array.isArray(existing.permission_overwrites) ? existing.permission_overwrites : []; - - let next = []; - if (mode === "replace") { - // keep @everyone deny if present, otherwise set one - const everyone = existingOverwrites.find((o) => String(o.id) === guildId && Number(o.type) === 0) || { - id: guildId, - type: 0, - allow: "0", - deny: String(BIT_VIEW_CHANNEL), - }; - next.push({ id: String(everyone.id), type: 0, allow: String(everyone.allow || "0"), deny: String(everyone.deny || BIT_VIEW_CHANNEL) }); - } else { - next = existingOverwrites.map((o) => ({ id: String(o.id), type: Number(o.type) === 1 ? 1 : 0, allow: String(o.allow || "0"), deny: String(o.deny || "0") })); - } - - const removeSet = new Set(removeTargetIds); - if (removeSet.size > 0) { - next = next.filter((o) => !removeSet.has(String(o.id))); - } - - const upsert = (id, type) => { - const idx = next.findIndex((o) => String(o.id) === String(id)); - const row = { id: String(id), type, allow: allowMask, deny: denyMask }; - if (idx >= 0) next[idx] = row; - else next.push(row); - }; - - for (const userId of addUserIds) upsert(userId, 1); - for (const roleId of addRoleIds) upsert(roleId, 0); - - const payload = { permission_overwrites: next }; - - if (body.dryRun === true) { - return { ok: true, action: "channel-private-update", dryRun: true, payload, mode }; - } - - const channel = await discordRequest(`/channels/${channelId}`, { - method: "PATCH", - body: JSON.stringify(payload), - }); - - return { ok: true, action: "channel-private-update", mode, channel }; -} - -async function actionMemberList(body) { - const guildId = String(body.guildId || "").trim(); - if (!guildId) throw fail(400, "bad_request", "guildId is required"); - ensureGuildAllowed(guildId); - - const limitRaw = Number(body.limit ?? 100); - const limit = Number.isFinite(limitRaw) ? Math.max(1, Math.min(1000, Math.floor(limitRaw))) : 100; - const after = body.after ? String(body.after) : undefined; - const fields = parseFieldList(body.fields); - if (fields.length > MAX_MEMBER_FIELDS) { - throw fail(400, "bad_request", "fields exceeds MAX_MEMBER_FIELDS", { - limit: MAX_MEMBER_FIELDS, - size: fields.length, - }); - } - - const qs = new URLSearchParams(); - qs.set("limit", String(limit)); - if (after) qs.set("after", after); - - const members = await discordRequest(`/guilds/${guildId}/members?${qs.toString()}`); - const projected = Array.isArray(members) ? members.map((m) => projectMember(m, fields)) : members; - - const response = { - ok: true, - action: "member-list", - guildId, - count: Array.isArray(projected) ? projected.length : 0, - fields: fields.length ? fields : undefined, - members: projected, - }; - - const bytes = Buffer.byteLength(JSON.stringify(response), "utf8"); - if (bytes > MAX_MEMBER_RESPONSE_BYTES) { - throw fail(413, "response_too_large", "member-list response exceeds MAX_MEMBER_RESPONSE_BYTES", { - bytes, - limit: MAX_MEMBER_RESPONSE_BYTES, - hint: "reduce limit or set fields projection", - }); - } - - return response; -} - -async function handleAction(body) { - const action = String(body.action || "").trim(); - if (!action) throw fail(400, "bad_request", "action is required"); - ensureActionEnabled(action); - - if (action === "channel-private-create") return await actionChannelPrivateCreate(body); - if (action === "channel-private-update") return await actionChannelPrivateUpdate(body); - if (action === "member-list") return await actionMemberList(body); - - throw fail(400, "unsupported_action", `unsupported action: ${action}`); -} - -const server = http.createServer((req, res) => { - if (req.method === "GET" && req.url === "/health") { - return sendJson(res, 200, { - ok: true, - service: "discord-control-api", - authRequired: !!authToken || requireAuthToken, - actionGates: enabledActions, - guildAllowlistEnabled: allowedGuildIds.size > 0, - limits: { - maxMemberFields: MAX_MEMBER_FIELDS, - maxMemberResponseBytes: MAX_MEMBER_RESPONSE_BYTES, - maxPrivateMutationTargets: MAX_PRIVATE_MUTATION_TARGETS, - }, - }); - } - - if (req.method !== "POST" || req.url !== "/v1/discord/action") { - return sendJson(res, 404, { error: "not_found" }); - } - - let body = ""; - req.on("data", (chunk) => { - body += chunk; - if (body.length > 2_000_000) req.destroy(); - }); - - req.on("end", async () => { - try { - ensureControlAuth(req); - const parsed = body ? JSON.parse(body) : {}; - const result = await handleAction(parsed); - return sendJson(res, 200, result); - } catch (err) { - return sendJson(res, err?.status || 500, { - error: err?.code || "request_failed", - message: String(err?.message || err), - details: err?.details, - }); - } - }); -}); - -server.listen(port, () => { - console.log(`[discord-control-api] listening on :${port}`); -}); diff --git a/docs/CONFIG.example.json b/docs/CONFIG.example.json index f2c75ff..84e3a80 100644 --- a/docs/CONFIG.example.json +++ b/docs/CONFIG.example.json @@ -21,7 +21,6 @@ "enableDirigentPolicyTool": true, "enableDebugLogs": false, "debugLogChannelIds": [], - "discordControlApiBaseUrl": "http://127.0.0.1:8790", "discordControlApiToken": "", "discordControlCallerId": "agent-main" } diff --git a/docs/DISCORD_CONTROL.md b/docs/DISCORD_CONTROL.md deleted file mode 100644 index de9119e..0000000 --- a/docs/DISCORD_CONTROL.md +++ /dev/null @@ -1,150 +0,0 @@ -# Discord Control API - -目标:补齐 OpenClaw 内置 message 工具当前未覆盖的两个能力: - -> 现在可以通过 Dirigent 插件内置的可选工具 `discord_control` 直接调用(无需手写 curl)。 -> 注意:该工具是 optional,需要在 agent tools allowlist 中显式允许(例如允许 `dirigent` 或 `discord_control`)。 - -1. 创建指定名单可见的私人频道 -2. 查看 server 成员列表(分页) - -## Start - -```bash -cd discord-control-api -export DISCORD_BOT_TOKEN='xxx' -# 建议启用 -export AUTH_TOKEN='strong-token' -# optional hard requirement -# export REQUIRE_AUTH_TOKEN=true -# optional action gates -# export ENABLE_CHANNEL_PRIVATE_CREATE=true -# export ENABLE_CHANNEL_PRIVATE_UPDATE=true -# export ENABLE_MEMBER_LIST=true -# optional allowlist -# export ALLOWED_GUILD_IDS='123,456' -# export ALLOWED_CALLER_IDS='agent-main,agent-admin' -# optional limits -# export MAX_MEMBER_FIELDS=20 -# export MAX_MEMBER_RESPONSE_BYTES=500000 -# export MAX_PRIVATE_MUTATION_TARGETS=200 -node server.mjs -``` - -Health: - -```bash -curl -sS http://127.0.0.1:8790/health -``` - -## Unified action endpoint - -`POST /v1/discord/action` - -- Header: `Authorization: Bearer `(若配置) -- Header: `X-OpenClaw-Caller-Id: `(若配置了 `ALLOWED_CALLER_IDS`) -- Body: `{ "action": "...", ... }` - ---- - -## Action: channel-private-create - -与 OpenClaw `channel-create` 参数保持风格一致,并增加私密覆盖参数。 - -### Request - -```json -{ - "action": "channel-private-create", - "guildId": "123", - "name": "private-room", - "type": 0, - "parentId": "456", - "topic": "secret", - "position": 3, - "nsfw": false, - "allowedUserIds": ["111", "222"], - "allowedRoleIds": ["333"], - "allowMask": "67648", - "denyEveryoneMask": "1024", - "dryRun": false -} -``` - -说明: -- 默认 deny `@everyone` 的 `VIEW_CHANNEL`。 -- 默认给 allowed targets 放行:`VIEW_CHANNEL + SEND_MESSAGES + READ_MESSAGE_HISTORY`。 -- `allowMask/denyEveryoneMask` 使用 Discord permission bit string。 - ---- - -## Action: channel-private-update - -对现有频道的白名单/覆盖权限做增删改。 - -### Request - -```json -{ - "action": "channel-private-update", - "guildId": "123", - "channelId": "789", - "mode": "merge", - "addUserIds": ["111"], - "addRoleIds": ["333"], - "removeTargetIds": ["222"], - "allowMask": "67648", - "denyMask": "0", - "dryRun": false -} -``` - -说明: -- `mode=merge`:在现有覆盖基础上增删 -- `mode=replace`:重建覆盖(保留/补上 @everyone deny) - ---- - -## Action: member-list - -### Request - -```json -{ - "action": "member-list", - "guildId": "123", - "limit": 100, - "after": "0", - "fields": ["user.id", "user.username", "nick", "roles", "joined_at"] -} -``` - -说明: -- `limit` 1~1000 -- `after` 用于分页(Discord snowflake) -- `fields` 可选:字段裁剪,减小返回体;可用 `user.xxx` 选子字段 - ---- - -## Curl examples - -示例文件:`docs/EXAMPLES.discord-control.json` - -快速检查脚本: - -```bash -./scripts/smoke-discord-control.sh -# with env -# AUTH_TOKEN=xxx CALLER_ID=agent-main GUILD_ID=123 CHANNEL_ID=456 ./scripts/smoke-discord-control.sh -``` - -## Notes - -鉴权与内置风格对齐(简化版): -- 控制面 token:`AUTH_TOKEN` / `REQUIRE_AUTH_TOKEN` -- 调用者 allowlist:`ALLOWED_CALLER_IDS`(配合 `X-OpenClaw-Caller-Id`) -- action gate:`ENABLE_CHANNEL_PRIVATE_CREATE` / `ENABLE_MEMBER_LIST` -- guild allowlist:`ALLOWED_GUILD_IDS` - -- 这不是 bot 自提权工具;bot 仍需由管理员授予足够权限。 -- 若无权限,Discord API 会返回 403 并透传错误细节。 diff --git a/package.json b/package.json index 0965160..f52b91a 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dist/", "plugin/", "no-reply-api/", - "discord-control-api/", + "docs/", "scripts/install-dirigent-openclaw.mjs", "docker-compose.yml", diff --git a/plugin/README.md b/plugin/README.md index cd6592d..add6333 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -39,7 +39,7 @@ Unified optional tool: - `bypassUserIds` (deprecated alias of `humanList`) - `endSymbols` (default ["🔚"]) - `enableDiscordControlTool` (default true) -- `discordControlApiBaseUrl` (default `http://127.0.0.1:8790`) +- Discord control actions are executed in-plugin via Discord REST API (no `discordControlApiBaseUrl` needed) - `discordControlApiToken` - `discordControlCallerId` - `enableDebugLogs` (default false) diff --git a/plugin/core/live-config.ts b/plugin/core/live-config.ts index 094ab19..b778c26 100644 --- a/plugin/core/live-config.ts +++ b/plugin/core/live-config.ts @@ -11,7 +11,6 @@ export function getLivePluginConfig(api: OpenClawPluginApi, fallback: DirigentCo return { enableDiscordControlTool: true, enableDirigentPolicyTool: true, - discordControlApiBaseUrl: "http://127.0.0.1:8790", enableDebugLogs: false, debugLogChannelIds: [], schedulingIdentifier: "➡️", diff --git a/plugin/index.ts b/plugin/index.ts index 1ecd89c..83483fa 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -56,15 +56,11 @@ export default { const baseConfig = { enableDiscordControlTool: true, enableDirigentPolicyTool: true, - discordControlApiBaseUrl: "http://127.0.0.1:8790", schedulingIdentifier: "➡️", waitIdentifier: "👤", ...(api.pluginConfig || {}), } as DirigentConfig & { enableDiscordControlTool: boolean; - discordControlApiBaseUrl: string; - discordControlApiToken?: string; - discordControlCallerId?: string; enableDirigentPolicyTool: boolean; }; diff --git a/plugin/openclaw.plugin.json b/plugin/openclaw.plugin.json index b901262..91cd1d6 100644 --- a/plugin/openclaw.plugin.json +++ b/plugin/openclaw.plugin.json @@ -22,9 +22,6 @@ "noReplyModel": { "type": "string" }, "enableDiscordControlTool": { "type": "boolean", "default": true }, "enableDirigentPolicyTool": { "type": "boolean", "default": true }, - "discordControlApiBaseUrl": { "type": "string", "default": "http://127.0.0.1:8790" }, - "discordControlApiToken": { "type": "string" }, - "discordControlCallerId": { "type": "string" }, "enableDebugLogs": { "type": "boolean", "default": false }, "debugLogChannelIds": { "type": "array", "items": { "type": "string" }, "default": [] }, "moderatorBotToken": { "type": "string" } diff --git a/scripts/smoke-discord-control.sh b/scripts/smoke-discord-control.sh deleted file mode 100755 index 613be5c..0000000 --- a/scripts/smoke-discord-control.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -BASE_URL="${BASE_URL:-http://127.0.0.1:8790}" -AUTH_TOKEN="${AUTH_TOKEN:-}" -CALLER_ID="${CALLER_ID:-}" - -AUTH_HEADER=() -if [[ -n "$AUTH_TOKEN" ]]; then - AUTH_HEADER=(-H "Authorization: Bearer ${AUTH_TOKEN}") -fi - -CALLER_HEADER=() -if [[ -n "$CALLER_ID" ]]; then - CALLER_HEADER=(-H "X-OpenClaw-Caller-Id: ${CALLER_ID}") -fi - -echo "[1] health" -curl -sS "${BASE_URL}/health" | sed -n '1,20p' - -if [[ -z "${GUILD_ID:-}" ]]; then - echo "skip action checks: set GUILD_ID (and optional CHANNEL_ID) to run dryRun actions" - exit 0 -fi - -echo "[2] dry-run private create" -curl -sS -X POST "${BASE_URL}/v1/discord/action" \ - -H 'Content-Type: application/json' \ - "${AUTH_HEADER[@]}" \ - "${CALLER_HEADER[@]}" \ - -d "{\"action\":\"channel-private-create\",\"guildId\":\"${GUILD_ID}\",\"name\":\"wg-dryrun\",\"dryRun\":true}" \ - | sed -n '1,80p' - -if [[ -n "${CHANNEL_ID:-}" ]]; then - echo "[3] dry-run private update" - curl -sS -X POST "${BASE_URL}/v1/discord/action" \ - -H 'Content-Type: application/json' \ - "${AUTH_HEADER[@]}" \ - "${CALLER_HEADER[@]}" \ - -d "{\"action\":\"channel-private-update\",\"guildId\":\"${GUILD_ID}\",\"channelId\":\"${CHANNEL_ID}\",\"mode\":\"merge\",\"dryRun\":true}" \ - | sed -n '1,100p' -fi - -echo "[4] member-list (limit=1)" -curl -sS -X POST "${BASE_URL}/v1/discord/action" \ - -H 'Content-Type: application/json' \ - "${AUTH_HEADER[@]}" \ - "${CALLER_HEADER[@]}" \ - -d "{\"action\":\"member-list\",\"guildId\":\"${GUILD_ID}\",\"limit\":1,\"fields\":[\"user.id\",\"user.username\"]}" \ - | sed -n '1,120p' - -echo "smoke-discord-control: done"