diff --git a/.github/workflows/nuke.yml b/.github/workflows/nuke.yml index 9a26d96d8..dd06611dc 100644 --- a/.github/workflows/nuke.yml +++ b/.github/workflows/nuke.yml @@ -70,6 +70,10 @@ on: description: "Eas" type: boolean default: false + discord: + description: "Discord" + type: boolean + default: false env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" diff --git a/.github/workflows/pr-package.yml b/.github/workflows/pr-package.yml index d2c6d918a..b05a6152d 100644 --- a/.github/workflows/pr-package.yml +++ b/.github/workflows/pr-package.yml @@ -36,7 +36,7 @@ jobs: if: contains(github.event.pull_request.labels.*.name, 'force-ci') run: | set -euo pipefail - all='["core","aws","cloudflare","gcp","neon","planetscale","prisma-postgres","stripe","supabase","posthog","axiom","azure","kubernetes","coinbase","mongodb-atlas","fly-io","turso","typesense","workos","expo-eas"]' + all='["core","aws","cloudflare","gcp","neon","planetscale","prisma-postgres","stripe","supabase","posthog","axiom","azure","kubernetes","coinbase","mongodb-atlas","fly-io","turso","typesense","workos","expo-eas","discord"]' echo "packages=${all}" >> "$GITHUB_OUTPUT" - uses: actions/checkout@v6 - uses: dorny/paths-filter@v4 @@ -102,6 +102,9 @@ jobs: expo-eas: - 'packages/expo-eas/**' - 'packages/core/**' + discord: + - 'packages/discord/**' + - 'packages/core/**' # ── Compute tags once so every matrix job + the comment use the same set. ─ tags: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7bf1d386..afe5fc62a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,6 +46,7 @@ env: packages/typesense/package.json packages/workos/package.json packages/expo-eas/package.json + packages/discord/package.json bun.lock CHANGELOG.md diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 30c3bc584..d2545dce9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,6 +42,7 @@ jobs: typesense: ${{ steps.force.outputs.all || steps.changes.outputs.typesense }} workos: ${{ steps.force.outputs.all || steps.changes.outputs.workos }} expo-eas: ${{ steps.force.outputs.all || steps.changes.outputs.expo-eas }} + discord: ${{ steps.force.outputs.all || steps.changes.outputs.discord }} steps: - id: force if: contains(github.event.pull_request.labels.*.name, 'force-ci') @@ -110,6 +111,9 @@ jobs: expo-eas: - 'packages/expo-eas/**' - 'packages/core/**' + discord: + - 'packages/discord/**' + - 'packages/core/**' ci-core: needs: detect-changes @@ -494,3 +498,25 @@ jobs: working-directory: packages/expo-eas env: EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + + ci-discord: + needs: detect-changes + if: needs.detect-changes.outputs.discord == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - run: bun install + - run: bun run build + working-directory: packages/core + - run: bun run check + working-directory: packages/discord + - run: bun run test + working-directory: packages/discord + env: + DISCORD_API_BASE_URL: ${{ secrets.DISCORD_API_BASE_URL }} + DISCORD_BEARER_TOKEN: ${{ secrets.DISCORD_BEARER_TOKEN }} + DISCORD_BOT_TOKEN: ${{ secrets.DISCORD_BOT_TOKEN }} + DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} diff --git a/bun.lock b/bun.lock index dd7f2ced2..ebe1fd6d8 100644 --- a/bun.lock +++ b/bun.lock @@ -132,6 +132,23 @@ "effect": "catalog:", }, }, + "packages/discord": { + "name": "@distilled.cloud/discord", + "version": "0.2.0-alpha", + "dependencies": { + "@distilled.cloud/core": "workspace:*", + "effect": "catalog:", + }, + "devDependencies": { + "@types/bun": "catalog:", + "@types/node": "catalog:", + "dotenv": "catalog:", + "vitest": "catalog:", + }, + "peerDependencies": { + "effect": "catalog:", + }, + }, "packages/expo-eas": { "name": "@distilled.cloud/expo-eas", "version": "0.17.0", @@ -479,6 +496,8 @@ "@distilled.cloud/core": ["@distilled.cloud/core@workspace:packages/core"], + "@distilled.cloud/discord": ["@distilled.cloud/discord@workspace:packages/discord"], + "@distilled.cloud/expo-eas": ["@distilled.cloud/expo-eas@workspace:packages/expo-eas"], "@distilled.cloud/fly-io": ["@distilled.cloud/fly-io@workspace:packages/fly-io"], diff --git a/packages/discord/README.md b/packages/discord/README.md new file mode 100644 index 000000000..9559f5299 --- /dev/null +++ b/packages/discord/README.md @@ -0,0 +1,87 @@ +# @distilled.cloud/discord + +Effect-native Discord SDK generated from the [Discord HTTP API specification](https://github.com/discord/discord-api-spec). Manage applications, guilds, channels, members, messages, interactions, webhooks, and more with exhaustive error typing. + +## Installation + +```bash +npm install @distilled.cloud/discord effect +``` + +## Quick Start + +```typescript +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { getMyUser } from "@distilled.cloud/discord/Operations"; +import { CredentialsFromEnv } from "@distilled.cloud/discord"; + +const program = Effect.gen(function* () { + const me = yield* getMyUser({}); + return me; +}); + +const DiscordLive = Layer.mergeAll(FetchHttpClient.layer, CredentialsFromEnv); + +program.pipe(Effect.provide(DiscordLive), Effect.runPromise); +``` + +## Configuration + +Set one of the following environment variables: + +```bash +# Bot token (most common) — sent as `Authorization: Bot ` +DISCORD_BOT_TOKEN=your-bot-token + +# Or, for OAuth2 — sent as `Authorization: Bearer ` +DISCORD_BEARER_TOKEN=your-bearer-token + +# Optional override +DISCORD_API_BASE_URL=https://discord.com/api/v10 +``` + +`DISCORD_TOKEN` is accepted as an alias for `DISCORD_BOT_TOKEN`. Create a bot +token in the [Discord Developer Portal](https://discord.com/developers/applications) +under **Your App > Bot**. + +## Error Handling + +```typescript +import { Effect } from "effect"; +import { getUser } from "@distilled.cloud/discord/Operations"; +import { + NotFound, + DiscordRateLimited, + UnknownDiscordError, +} from "@distilled.cloud/discord/Errors"; + +getUser({ user_id: "0" }).pipe( + Effect.catchTags({ + NotFound: (_e: NotFound) => Effect.succeed(null), + DiscordRateLimited: (e: DiscordRateLimited) => + Effect.fail(new Error(`Rate limited; retry after ${e.retryAfter}s`)), + UnknownDiscordError: (e: UnknownDiscordError) => + Effect.fail(new Error(`Unknown: ${e.message}`)), + }), +); +``` + +## Services + +Operations cover the full Discord v10 HTTP API. Notable groups: + +- **Applications** — get/update application, commands, emojis, entitlements, role connections +- **Guilds** — create, get, update, delete; members, roles, bans, invites, integrations, widgets, scheduled events, audit log +- **Channels** — create, get, update, delete; permissions, invites, pins, threads, followers +- **Messages** — create, get, list, update, delete, crosspost, reactions, bulk delete +- **Interactions** — respond, edit, delete; webhook follow-ups +- **Users** — current user, DMs, connections, guilds +- **Webhooks** — create, get, update, delete, execute (incl. Slack/GitHub-compatible) +- **Stage Instances, Stickers, Soundboard, Lobbies, Polls, Voice Regions, OAuth2** + +See `src/operations/` for the full list (200+ operations). + +## License + +MIT diff --git a/packages/discord/package.json b/packages/discord/package.json new file mode 100644 index 000000000..dd5eb66fb --- /dev/null +++ b/packages/discord/package.json @@ -0,0 +1,88 @@ +{ + "name": "@distilled.cloud/discord", + "version": "0.2.0-alpha", + "repository": { + "type": "git", + "url": "https://github.com/alchemy-run/distilled", + "directory": "packages/discord" + }, + "type": "module", + "sideEffects": false, + "module": "src/index.ts", + "files": [ + "lib", + "src" + ], + "exports": { + ".": { + "types": "./lib/index.d.ts", + "bun": "./src/index.ts", + "default": "./lib/index.js" + }, + "./Category": { + "types": "./lib/category.d.ts", + "bun": "./src/category.ts", + "default": "./lib/category.js" + }, + "./Client": { + "types": "./lib/client.d.ts", + "bun": "./src/client.ts", + "default": "./lib/client.js" + }, + "./Credentials": { + "types": "./lib/credentials.d.ts", + "bun": "./src/credentials.ts", + "default": "./lib/credentials.js" + }, + "./Errors": { + "types": "./lib/errors.d.ts", + "bun": "./src/errors.ts", + "default": "./lib/errors.js" + }, + "./Operations": { + "types": "./lib/operations/index.d.ts", + "bun": "./src/operations/index.ts", + "default": "./lib/operations/index.js" + }, + "./Retry": { + "types": "./lib/retry.d.ts", + "bun": "./src/retry.ts", + "default": "./lib/retry.js" + }, + "./Sensitive": { + "types": "./lib/sensitive.d.ts", + "bun": "./src/sensitive.ts", + "default": "./lib/sensitive.js" + }, + "./Traits": { + "types": "./lib/traits.d.ts", + "bun": "./src/traits.ts", + "default": "./lib/traits.js" + } + }, + "scripts": { + "typecheck": "tsgo", + "build": "tsgo -b", + "fmt": "oxfmt --write src", + "lint": "oxlint --fix src", + "check": "tsgo && oxlint src && oxfmt --check src", + "test": "bunx vitest run test --passWithNoTests", + "publish:npm": "bun run build && bun publish --access public", + "generate": "bun run scripts/generate.ts && oxlint --fix src && oxfmt --write src && oxfmt --write src", + "specs:fetch": "echo 'No specs configured'", + "specs:update": "echo 'No specs configured'" + }, + "dependencies": { + "@distilled.cloud/core": "workspace:*", + "effect": "catalog:" + }, + "devDependencies": { + "@types/bun": "catalog:", + "@types/node": "catalog:", + "dotenv": "catalog:", + "vitest": "catalog:" + }, + "peerDependencies": { + "effect": "catalog:" + } +} \ No newline at end of file diff --git a/packages/discord/patches/001-expand-4xx.patch.json b/packages/discord/patches/001-expand-4xx.patch.json new file mode 100644 index 000000000..d47c93fe0 --- /dev/null +++ b/packages/discord/patches/001-expand-4xx.patch.json @@ -0,0 +1,4898 @@ +{ + "description": "Expand the blanket '4XX' response on each operation into explicit 400, 403, and 404 responses so the OpenAPI generator emits typed BadRequest/Forbidden/NotFound errors per operation. Every 4XX response in the Discord spec uses the same ClientErrorResponse envelope, so this is a faithful expansion.", + "patches": [ + { + "op": "add", + "path": "/paths/~1applications~1@me/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1@me/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1@me/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1@me/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1@me/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1@me/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1activity-instances~1{instance_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1activity-instances~1{instance_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1activity-instances~1{instance_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1attachment/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1attachment/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1attachment/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1commands/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1commands/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1commands/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1commands/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1commands/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1commands/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1commands/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1commands/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1commands/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1commands~1{command_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1commands~1{command_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1commands~1{command_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1commands~1{command_id}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1commands~1{command_id}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1commands~1{command_id}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1commands~1{command_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1commands~1{command_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1commands~1{command_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1emojis/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1emojis/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1emojis/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1emojis/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1emojis/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1emojis/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1emojis~1{emoji_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1emojis~1{emoji_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1emojis~1{emoji_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1emojis~1{emoji_id}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1emojis~1{emoji_id}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1emojis~1{emoji_id}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1emojis~1{emoji_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1emojis~1{emoji_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1emojis~1{emoji_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1entitlements/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1entitlements/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1entitlements/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1entitlements/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1entitlements/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1entitlements/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1entitlements~1{entitlement_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1entitlements~1{entitlement_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1entitlements~1{entitlement_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1entitlements~1{entitlement_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1entitlements~1{entitlement_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1entitlements~1{entitlement_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1entitlements~1{entitlement_id}~1consume/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1entitlements~1{entitlement_id}~1consume/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1entitlements~1{entitlement_id}~1consume/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands~1permissions/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands~1permissions/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands~1permissions/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands~1{command_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands~1{command_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands~1{command_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands~1{command_id}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands~1{command_id}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands~1{command_id}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands~1{command_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands~1{command_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands~1{command_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands~1{command_id}~1permissions/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands~1{command_id}~1permissions/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands~1{command_id}~1permissions/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands~1{command_id}~1permissions/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands~1{command_id}~1permissions/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1guilds~1{guild_id}~1commands~1{command_id}~1permissions/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1role-connections~1metadata/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1role-connections~1metadata/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1role-connections~1metadata/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1role-connections~1metadata/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1role-connections~1metadata/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1applications~1{application_id}~1role-connections~1metadata/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1followers/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1followers/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1followers/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1invites/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1invites/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1invites/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1invites/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1invites/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1invites/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1bulk-delete/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1bulk-delete/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1bulk-delete/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1pins/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1pins/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1pins/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1pins~1{message_id}/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1pins~1{message_id}/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1pins~1{message_id}/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1pins~1{message_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1pins~1{message_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1pins~1{message_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1crosspost/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1crosspost/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1crosspost/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1reactions/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1reactions/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1reactions/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1reactions~1{emoji_name}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1reactions~1{emoji_name}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1reactions~1{emoji_name}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1reactions~1{emoji_name}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1reactions~1{emoji_name}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1reactions~1{emoji_name}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1reactions~1{emoji_name}~1@me/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1reactions~1{emoji_name}~1@me/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1reactions~1{emoji_name}~1@me/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1reactions~1{emoji_name}~1@me/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1reactions~1{emoji_name}~1@me/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1reactions~1{emoji_name}~1@me/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1reactions~1{emoji_name}~1{user_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1reactions~1{emoji_name}~1{user_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1reactions~1{emoji_name}~1{user_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1threads/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1threads/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1messages~1{message_id}~1threads/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1permissions~1{overwrite_id}/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1permissions~1{overwrite_id}/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1permissions~1{overwrite_id}/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1permissions~1{overwrite_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1permissions~1{overwrite_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1permissions~1{overwrite_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1pins/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1pins/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1pins/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1pins~1{message_id}/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1pins~1{message_id}/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1pins~1{message_id}/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1pins~1{message_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1pins~1{message_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1pins~1{message_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1polls~1{message_id}~1answers~1{answer_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1polls~1{message_id}~1answers~1{answer_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1polls~1{message_id}~1answers~1{answer_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1polls~1{message_id}~1expire/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1polls~1{message_id}~1expire/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1polls~1{message_id}~1expire/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1recipients~1{user_id}/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1recipients~1{user_id}/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1recipients~1{user_id}/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1recipients~1{user_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1recipients~1{user_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1recipients~1{user_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1send-soundboard-sound/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1send-soundboard-sound/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1send-soundboard-sound/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1thread-members/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1thread-members/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1thread-members/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1thread-members~1@me/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1thread-members~1@me/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1thread-members~1@me/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1thread-members~1@me/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1thread-members~1@me/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1thread-members~1@me/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1thread-members~1{user_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1thread-members~1{user_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1thread-members~1{user_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1thread-members~1{user_id}/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1thread-members~1{user_id}/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1thread-members~1{user_id}/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1thread-members~1{user_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1thread-members~1{user_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1thread-members~1{user_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1threads/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1threads/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1threads/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1threads~1archived~1private/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1threads~1archived~1private/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1threads~1archived~1private/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1threads~1archived~1public/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1threads~1archived~1public/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1threads~1archived~1public/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1threads~1search/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1threads~1search/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1threads~1search/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1typing/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1typing/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1typing/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1users~1@me~1threads~1archived~1private/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1users~1@me~1threads~1archived~1private/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1users~1@me~1threads~1archived~1private/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1voice-status/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1voice-status/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1voice-status/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1webhooks/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1webhooks/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1webhooks/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1webhooks/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1webhooks/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1channels~1{channel_id}~1webhooks/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1gateway/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1gateway/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1gateway/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1gateway~1bot/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1gateway~1bot/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1gateway~1bot/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1templates~1{code}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1templates~1{code}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1templates~1{code}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1audit-logs/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1audit-logs/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1audit-logs/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1auto-moderation~1rules/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1auto-moderation~1rules/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1auto-moderation~1rules/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1auto-moderation~1rules/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1auto-moderation~1rules/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1auto-moderation~1rules/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1auto-moderation~1rules~1{rule_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1auto-moderation~1rules~1{rule_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1auto-moderation~1rules~1{rule_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1auto-moderation~1rules~1{rule_id}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1auto-moderation~1rules~1{rule_id}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1auto-moderation~1rules~1{rule_id}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1auto-moderation~1rules~1{rule_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1auto-moderation~1rules~1{rule_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1auto-moderation~1rules~1{rule_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1bans/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1bans/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1bans/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1bans~1{user_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1bans~1{user_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1bans~1{user_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1bans~1{user_id}/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1bans~1{user_id}/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1bans~1{user_id}/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1bans~1{user_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1bans~1{user_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1bans~1{user_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1bulk-ban/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1bulk-ban/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1bulk-ban/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1channels/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1channels/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1channels/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1channels/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1channels/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1channels/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1channels/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1channels/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1channels/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1emojis/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1emojis/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1emojis/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1emojis/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1emojis/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1emojis/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1emojis~1{emoji_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1emojis~1{emoji_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1emojis~1{emoji_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1emojis~1{emoji_id}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1emojis~1{emoji_id}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1emojis~1{emoji_id}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1emojis~1{emoji_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1emojis~1{emoji_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1emojis~1{emoji_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1integrations/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1integrations/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1integrations/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1integrations~1{integration_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1integrations~1{integration_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1integrations~1{integration_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1invites/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1invites/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1invites/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1@me/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1@me/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1@me/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1search/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1search/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1search/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1{user_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1{user_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1{user_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1{user_id}/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1{user_id}/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1{user_id}/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1{user_id}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1{user_id}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1{user_id}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1{user_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1{user_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1{user_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1{user_id}~1roles~1{role_id}/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1{user_id}~1roles~1{role_id}/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1{user_id}~1roles~1{role_id}/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1{user_id}~1roles~1{role_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1{user_id}~1roles~1{role_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1members~1{user_id}~1roles~1{role_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1messages~1search/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1messages~1search/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1messages~1search/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1new-member-welcome/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1new-member-welcome/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1new-member-welcome/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1onboarding/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1onboarding/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1onboarding/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1onboarding/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1onboarding/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1onboarding/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1preview/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1preview/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1preview/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1prune/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1prune/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1prune/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1prune/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1prune/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1prune/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1regions/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1regions/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1regions/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1requests/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1requests/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1requests/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1requests~1{request_id}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1requests~1{request_id}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1requests~1{request_id}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1roles/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1roles/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1roles/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1roles/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1roles/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1roles/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1roles/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1roles/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1roles/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1roles~1member-counts/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1roles~1member-counts/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1roles~1member-counts/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1roles~1{role_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1roles~1{role_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1roles~1{role_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1roles~1{role_id}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1roles~1{role_id}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1roles~1{role_id}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1roles~1{role_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1roles~1{role_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1roles~1{role_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1scheduled-events/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1scheduled-events/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1scheduled-events/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1scheduled-events/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1scheduled-events/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1scheduled-events/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1scheduled-events~1{guild_scheduled_event_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1scheduled-events~1{guild_scheduled_event_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1scheduled-events~1{guild_scheduled_event_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1scheduled-events~1{guild_scheduled_event_id}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1scheduled-events~1{guild_scheduled_event_id}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1scheduled-events~1{guild_scheduled_event_id}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1scheduled-events~1{guild_scheduled_event_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1scheduled-events~1{guild_scheduled_event_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1scheduled-events~1{guild_scheduled_event_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1scheduled-events~1{guild_scheduled_event_id}~1users/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1scheduled-events~1{guild_scheduled_event_id}~1users/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1scheduled-events~1{guild_scheduled_event_id}~1users/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1soundboard-sounds/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1soundboard-sounds/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1soundboard-sounds/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1soundboard-sounds/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1soundboard-sounds/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1soundboard-sounds/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1soundboard-sounds~1{sound_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1soundboard-sounds~1{sound_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1soundboard-sounds~1{sound_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1soundboard-sounds~1{sound_id}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1soundboard-sounds~1{sound_id}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1soundboard-sounds~1{sound_id}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1soundboard-sounds~1{sound_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1soundboard-sounds~1{sound_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1soundboard-sounds~1{sound_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1stickers/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1stickers/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1stickers/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1stickers/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1stickers/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1stickers/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1stickers~1{sticker_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1stickers~1{sticker_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1stickers~1{sticker_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1stickers~1{sticker_id}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1stickers~1{sticker_id}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1stickers~1{sticker_id}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1stickers~1{sticker_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1stickers~1{sticker_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1stickers~1{sticker_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1templates/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1templates/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1templates/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1templates/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1templates/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1templates/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1templates~1{code}/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1templates~1{code}/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1templates~1{code}/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1templates~1{code}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1templates~1{code}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1templates~1{code}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1templates~1{code}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1templates~1{code}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1templates~1{code}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1threads~1active/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1threads~1active/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1threads~1active/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1vanity-url/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1vanity-url/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1vanity-url/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1voice-states~1@me/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1voice-states~1@me/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1voice-states~1@me/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1voice-states~1@me/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1voice-states~1@me/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1voice-states~1@me/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1voice-states~1{user_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1voice-states~1{user_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1voice-states~1{user_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1voice-states~1{user_id}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1voice-states~1{user_id}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1voice-states~1{user_id}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1webhooks/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1webhooks/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1webhooks/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1welcome-screen/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1welcome-screen/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1welcome-screen/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1welcome-screen/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1welcome-screen/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1welcome-screen/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1widget/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1widget/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1widget/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1widget/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1widget/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1widget/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1widget.json/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1widget.json/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1widget.json/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1widget.png/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1widget.png/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1guilds~1{guild_id}~1widget.png/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1interactions~1{interaction_id}~1{interaction_token}~1callback/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1interactions~1{interaction_id}~1{interaction_token}~1callback/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1interactions~1{interaction_id}~1{interaction_token}~1callback/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1invites~1{code}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1invites~1{code}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1invites~1{code}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1invites~1{code}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1invites~1{code}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1invites~1{code}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1invites~1{code}~1target-users/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1invites~1{code}~1target-users/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1invites~1{code}~1target-users/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1invites~1{code}~1target-users/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1invites~1{code}~1target-users/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1invites~1{code}~1target-users/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1invites~1{code}~1target-users~1job-status/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1invites~1{code}~1target-users~1job-status/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1invites~1{code}~1target-users~1job-status/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1channel-linking/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1channel-linking/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1channel-linking/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1members~1@me/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1members~1@me/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1members~1@me/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1members~1@me~1invites/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1members~1@me~1invites/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1members~1@me~1invites/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1members~1bulk/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1members~1bulk/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1members~1bulk/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1members~1{user_id}/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1members~1{user_id}/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1members~1{user_id}/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1members~1{user_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1members~1{user_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1members~1{user_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1members~1{user_id}~1invites/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1members~1{user_id}~1invites/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1members~1{user_id}~1invites/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1messages/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1messages/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1messages/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1messages/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1messages/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1messages/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1messages~1{message_id}~1moderation-metadata/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1messages~1{message_id}~1moderation-metadata/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1lobbies~1{lobby_id}~1messages~1{message_id}~1moderation-metadata/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1oauth2~1@me/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1oauth2~1@me/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1oauth2~1@me/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1oauth2~1applications~1@me/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1oauth2~1applications~1@me/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1oauth2~1applications~1@me/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1oauth2~1keys/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1oauth2~1keys/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1oauth2~1keys/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1oauth2~1userinfo/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1oauth2~1userinfo/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1oauth2~1userinfo/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1partner-sdk~1dms~1{user_id_1}~1{user_id_2}~1messages~1{message_id}~1moderation-metadata/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1partner-sdk~1dms~1{user_id_1}~1{user_id_2}~1messages~1{message_id}~1moderation-metadata/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1partner-sdk~1dms~1{user_id_1}~1{user_id_2}~1messages~1{message_id}~1moderation-metadata/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1partner-sdk~1provisional-accounts~1unmerge/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1partner-sdk~1provisional-accounts~1unmerge/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1partner-sdk~1provisional-accounts~1unmerge/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1partner-sdk~1provisional-accounts~1unmerge~1bot/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1partner-sdk~1provisional-accounts~1unmerge~1bot/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1partner-sdk~1provisional-accounts~1unmerge~1bot/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1partner-sdk~1token/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1partner-sdk~1token/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1partner-sdk~1token/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1partner-sdk~1token~1bot/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1partner-sdk~1token~1bot/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1partner-sdk~1token~1bot/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1soundboard-default-sounds/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1soundboard-default-sounds/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1soundboard-default-sounds/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1stage-instances/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1stage-instances/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1stage-instances/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1stage-instances~1{channel_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1stage-instances~1{channel_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1stage-instances~1{channel_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1stage-instances~1{channel_id}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1stage-instances~1{channel_id}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1stage-instances~1{channel_id}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1stage-instances~1{channel_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1stage-instances~1{channel_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1stage-instances~1{channel_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1sticker-packs/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1sticker-packs/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1sticker-packs/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1sticker-packs~1{pack_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1sticker-packs~1{pack_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1sticker-packs~1{pack_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1stickers~1{sticker_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1stickers~1{sticker_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1stickers~1{sticker_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1applications~1{application_id}~1entitlements/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1applications~1{application_id}~1entitlements/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1applications~1{application_id}~1entitlements/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1applications~1{application_id}~1role-connection/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1applications~1{application_id}~1role-connection/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1applications~1{application_id}~1role-connection/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1applications~1{application_id}~1role-connection/put/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1applications~1{application_id}~1role-connection/put/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1applications~1{application_id}~1role-connection/put/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1applications~1{application_id}~1role-connection/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1applications~1{application_id}~1role-connection/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1applications~1{application_id}~1role-connection/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1channels/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1channels/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1channels/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1connections/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1connections/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1connections/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1guilds/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1guilds/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1guilds/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1guilds~1{guild_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1guilds~1{guild_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1guilds~1{guild_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1guilds~1{guild_id}~1member/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1guilds~1{guild_id}~1member/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1@me~1guilds~1{guild_id}~1member/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1{user_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1{user_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1users~1{user_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1voice~1regions/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1voice~1regions/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1voice~1regions/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1github/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1github/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1github/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1messages~1@original/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1messages~1@original/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1messages~1@original/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1messages~1@original/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1messages~1@original/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1messages~1@original/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1messages~1@original/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1messages~1@original/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1messages~1@original/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1messages~1{message_id}/get/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1messages~1{message_id}/get/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1messages~1{message_id}/get/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1messages~1{message_id}/patch/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1messages~1{message_id}/patch/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1messages~1{message_id}/patch/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1messages~1{message_id}/delete/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1messages~1{message_id}/delete/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1messages~1{message_id}/delete/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1slack/post/responses/400", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1slack/post/responses/403", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}~1{webhook_token}~1slack/post/responses/404", + "value": { + "$ref": "#/components/responses/ClientErrorResponse" + } + } + ] +} diff --git a/packages/discord/scripts/generate.ts b/packages/discord/scripts/generate.ts new file mode 100644 index 000000000..84e7f6988 --- /dev/null +++ b/packages/discord/scripts/generate.ts @@ -0,0 +1,26 @@ +/** + * Discord SDK Code Generator + * + * Uses the shared OpenAPI generator from sdk-core to generate operations + * from the Discord HTTP API OpenAPI 3.1 spec. + */ +import * as path from "path"; +import { generateFromOpenAPI } from "@distilled.cloud/core/openapi/generate"; + +const rootDir = path.join(import.meta.dir, ".."); + +generateFromOpenAPI({ + specPath: path.join( + rootDir, + "specs/discord-api-spec/specs/openapi.json", + ), + patchDir: path.join(rootDir, "patches"), + outputDir: path.join(rootDir, "src/operations"), + importPrefix: "..", + clientImport: "../client", + traitsImport: "../traits", + sensitiveImport: "../sensitive", + errorsImport: "../errors", + includeOperationErrors: true, + skipDeprecated: true, +}); diff --git a/packages/discord/scripts/nuke.ts b/packages/discord/scripts/nuke.ts new file mode 100644 index 000000000..248860c36 --- /dev/null +++ b/packages/discord/scripts/nuke.ts @@ -0,0 +1,1147 @@ +#!/usr/bin/env bun +/** + * Discord Nuke Script + * + * Lists and deletes resources owned/managed by the authenticated Discord + * application/bot. Supports --dry-run to preview without deleting. + * + * Cleans up: + * - Global application commands and application emojis (account-level) + * - Per-guild (for every guild the bot is a member of): + * guild application commands, scheduled events, auto-moderation rules, + * integrations, soundboard sounds, emojis, stickers, templates, + * webhooks (owned by this application), channels and roles (when the + * bot has permissions to manage them). + * + * The script never leaves guilds and never deletes the @everyone role, + * managed roles, or roles/emojis owned by other integrations. + * + * Usage: + * bun packages/discord/scripts/nuke.ts --dry-run + * bun packages/discord/scripts/nuke.ts + */ +import { config } from "dotenv"; +import * as fs from "node:fs"; +import * as nodePath from "node:path"; + +// Load .env from repo root (two levels up from scripts/) +const envPath = nodePath.resolve(import.meta.dir, "../../../.env"); +config({ path: envPath }); +if ( + !process.env.DISCORD_BOT_TOKEN && + !process.env.DISCORD_TOKEN && + !process.env.DISCORD_BEARER_TOKEN +) { + // Also try CWD/.env as fallback + config(); +} + +import { BunRuntime, BunServices } from "@effect/platform-bun"; +import { Console, Effect } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { Command, Flag } from "effect/unstable/cli"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { getMyApplication } from "../src/operations/getMyApplication.ts"; +import { listMyGuilds } from "../src/operations/listMyGuilds.ts"; +import { listApplicationCommands } from "../src/operations/listApplicationCommands.ts"; +import { deleteApplicationCommand } from "../src/operations/deleteApplicationCommand.ts"; +import { listApplicationEmojis } from "../src/operations/listApplicationEmojis.ts"; +import { deleteApplicationEmoji } from "../src/operations/deleteApplicationEmoji.ts"; +import { listGuildApplicationCommands } from "../src/operations/listGuildApplicationCommands.ts"; +import { deleteGuildApplicationCommand } from "../src/operations/deleteGuildApplicationCommand.ts"; +import { listGuildScheduledEvents } from "../src/operations/listGuildScheduledEvents.ts"; +import { deleteGuildScheduledEvent } from "../src/operations/deleteGuildScheduledEvent.ts"; +import { listAutoModerationRules } from "../src/operations/listAutoModerationRules.ts"; +import { deleteAutoModerationRule } from "../src/operations/deleteAutoModerationRule.ts"; +import { listGuildIntegrations } from "../src/operations/listGuildIntegrations.ts"; +import { deleteGuildIntegration } from "../src/operations/deleteGuildIntegration.ts"; +import { listGuildSoundboardSounds } from "../src/operations/listGuildSoundboardSounds.ts"; +import { deleteGuildSoundboardSound } from "../src/operations/deleteGuildSoundboardSound.ts"; +import { listGuildEmojis } from "../src/operations/listGuildEmojis.ts"; +import { deleteGuildEmoji } from "../src/operations/deleteGuildEmoji.ts"; +import { listGuildStickers } from "../src/operations/listGuildStickers.ts"; +import { deleteGuildSticker } from "../src/operations/deleteGuildSticker.ts"; +import { listGuildTemplates } from "../src/operations/listGuildTemplates.ts"; +import { deleteGuildTemplate } from "../src/operations/deleteGuildTemplate.ts"; +import { getGuildWebhooks } from "../src/operations/getGuildWebhooks.ts"; +import { deleteWebhook } from "../src/operations/deleteWebhook.ts"; +import { listGuildChannels } from "../src/operations/listGuildChannels.ts"; +import { deleteChannel } from "../src/operations/deleteChannel.ts"; +import { listGuildRoles } from "../src/operations/listGuildRoles.ts"; +import { deleteGuildRole } from "../src/operations/deleteGuildRole.ts"; + +// ANSI colors +const RED = "\x1b[31m"; +const GREEN = "\x1b[32m"; +const YELLOW = "\x1b[33m"; +const CYAN = "\x1b[36m"; +const BOLD = "\x1b[1m"; +const DIM = "\x1b[2m"; +const RESET = "\x1b[0m"; + +// Counters +let totalFound = 0; +let totalSkipped = 0; +let totalDeleted = 0; +let totalFailed = 0; + +// ============================================================================ +// Nuke Config +// ============================================================================ + +interface ExcludeRule { + type: string; + ids?: string[]; + namePatterns?: string[]; + reason?: string; +} + +interface NukeConfig { + exclude?: ExcludeRule[]; +} + +const PKG_DIR = nodePath.resolve(import.meta.dir, ".."); + +function loadNukeConfig(): NukeConfig { + const p = nodePath.join(PKG_DIR, "nuke-config.json"); + if (!fs.existsSync(p)) return {}; + return JSON.parse(fs.readFileSync(p, "utf-8")); +} + +function matchGlob(pattern: string, value: string): boolean { + return new RegExp("^" + pattern.replace(/\*/g, ".*") + "$").test(value); +} + +function isExcluded( + config: NukeConfig, + type: string, + id: string, + name?: string, +): ExcludeRule | undefined { + return config.exclude?.find((rule) => { + if (rule.type !== type) return false; + if (rule.ids?.includes(id)) return true; + if (name && rule.namePatterns?.some((p) => matchGlob(p, name))) return true; + return false; + }); +} + +// ============================================================================ +// Resource operations (application-level) +// ============================================================================ + +const nukeGlobalCommands = ( + dryRun: boolean, + nukeConfig: NukeConfig, + applicationId: string, +) => + Effect.gen(function* () { + yield* Console.log(`\n${BOLD}${CYAN}Global Application Commands${RESET}`); + + const cmds = yield* listApplicationCommands({ + application_id: applicationId, + }).pipe( + Effect.catch(() => + Console.log( + ` ${RED}Failed to list global application commands${RESET}`, + ).pipe( + Effect.map( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + () => [] as any[], + ), + ), + ), + ); + + if (cmds.length === 0) { + yield* Console.log(` ${DIM}No global commands found${RESET}`); + return; + } + + for (const cmd of cmds) { + totalFound++; + const excluded = isExcluded( + nukeConfig, + "ApplicationCommand", + cmd.id, + cmd.name, + ); + if (excluded) { + totalSkipped++; + yield* Console.log( + ` ${YELLOW}[SKIP]${RESET} ApplicationCommand: ${cmd.name} ${DIM}(${cmd.id})${RESET} — ${excluded.reason ?? "excluded"}`, + ); + continue; + } + + if (dryRun) { + yield* Console.log( + ` ${RED}[DELETE]${RESET} ApplicationCommand: ${cmd.name} ${DIM}(${cmd.id})${RESET}`, + ); + } else { + yield* Console.log( + ` ${RED}[DELETE]${RESET} ApplicationCommand: ${cmd.name} ${DIM}(${cmd.id})${RESET}`, + ); + yield* deleteApplicationCommand({ + application_id: applicationId, + command_id: cmd.id, + }).pipe( + Effect.andThen(() => { + totalDeleted++; + }), + Effect.catch(() => { + totalFailed++; + return Console.log( + ` ${RED}Failed to delete command ${cmd.id}${RESET}`, + ); + }), + ); + } + } + }); + +const nukeApplicationEmojis = ( + dryRun: boolean, + nukeConfig: NukeConfig, + applicationId: string, +) => + Effect.gen(function* () { + yield* Console.log(`\n${BOLD}${CYAN}Application Emojis${RESET}`); + + const result = yield* listApplicationEmojis({ + application_id: applicationId, + }).pipe( + Effect.catch(() => + Console.log( + ` ${RED}Failed to list application emojis${RESET}`, + ).pipe( + Effect.map( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + () => ({ items: [] as any[] }), + ), + ), + ), + ); + + const items = result.items ?? []; + if (items.length === 0) { + yield* Console.log(` ${DIM}No application emojis found${RESET}`); + return; + } + + for (const emoji of items) { + totalFound++; + const excluded = isExcluded( + nukeConfig, + "ApplicationEmoji", + emoji.id, + emoji.name, + ); + if (excluded) { + totalSkipped++; + yield* Console.log( + ` ${YELLOW}[SKIP]${RESET} ApplicationEmoji: ${emoji.name} ${DIM}(${emoji.id})${RESET} — ${excluded.reason ?? "excluded"}`, + ); + continue; + } + + if (dryRun) { + yield* Console.log( + ` ${RED}[DELETE]${RESET} ApplicationEmoji: ${emoji.name} ${DIM}(${emoji.id})${RESET}`, + ); + } else { + yield* Console.log( + ` ${RED}[DELETE]${RESET} ApplicationEmoji: ${emoji.name} ${DIM}(${emoji.id})${RESET}`, + ); + yield* deleteApplicationEmoji({ + application_id: applicationId, + emoji_id: emoji.id, + }).pipe( + Effect.andThen(() => { + totalDeleted++; + }), + Effect.catch(() => { + totalFailed++; + return Console.log( + ` ${RED}Failed to delete emoji ${emoji.id}${RESET}`, + ); + }), + ); + } + } + }); + +// ============================================================================ +// Resource operations (guild-level) +// ============================================================================ + +const nukeGuildCommands = ( + dryRun: boolean, + nukeConfig: NukeConfig, + applicationId: string, + guildId: string, +) => + Effect.gen(function* () { + const cmds = yield* listGuildApplicationCommands({ + application_id: applicationId, + guild_id: guildId, + }).pipe( + Effect.catch(() => + Console.log( + ` ${RED}Failed to list guild commands for ${guildId}${RESET}`, + ).pipe( + Effect.map( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + () => [] as any[], + ), + ), + ), + ); + + for (const cmd of cmds) { + totalFound++; + const excluded = isExcluded( + nukeConfig, + "GuildApplicationCommand", + cmd.id, + cmd.name, + ); + if (excluded) { + totalSkipped++; + yield* Console.log( + ` ${YELLOW}[SKIP]${RESET} GuildApplicationCommand: ${cmd.name} ${DIM}(${cmd.id})${RESET} — ${excluded.reason ?? "excluded"}`, + ); + continue; + } + + if (dryRun) { + yield* Console.log( + ` ${RED}[DELETE]${RESET} GuildApplicationCommand: ${cmd.name} ${DIM}(${cmd.id})${RESET}`, + ); + } else { + yield* Console.log( + ` ${RED}[DELETE]${RESET} GuildApplicationCommand: ${cmd.name} ${DIM}(${cmd.id})${RESET}`, + ); + yield* deleteGuildApplicationCommand({ + application_id: applicationId, + guild_id: guildId, + command_id: cmd.id, + }).pipe( + Effect.andThen(() => { + totalDeleted++; + }), + Effect.catch(() => { + totalFailed++; + return Console.log( + ` ${RED}Failed to delete guild command ${cmd.id}${RESET}`, + ); + }), + ); + } + } + }); + +const nukeScheduledEvents = ( + dryRun: boolean, + nukeConfig: NukeConfig, + guildId: string, +) => + Effect.gen(function* () { + const events = yield* listGuildScheduledEvents({ guild_id: guildId }).pipe( + Effect.catch(() => + Console.log( + ` ${RED}Failed to list scheduled events for ${guildId}${RESET}`, + ).pipe( + Effect.map( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + () => [] as any[], + ), + ), + ), + ); + + for (const evt of events) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const e = evt as any; + const id = String(e.id ?? ""); + const name = String(e.name ?? ""); + if (!id) continue; + + totalFound++; + const excluded = isExcluded(nukeConfig, "ScheduledEvent", id, name); + if (excluded) { + totalSkipped++; + yield* Console.log( + ` ${YELLOW}[SKIP]${RESET} ScheduledEvent: ${name} ${DIM}(${id})${RESET} — ${excluded.reason ?? "excluded"}`, + ); + continue; + } + + if (dryRun) { + yield* Console.log( + ` ${RED}[DELETE]${RESET} ScheduledEvent: ${name} ${DIM}(${id})${RESET}`, + ); + } else { + yield* Console.log( + ` ${RED}[DELETE]${RESET} ScheduledEvent: ${name} ${DIM}(${id})${RESET}`, + ); + yield* deleteGuildScheduledEvent({ + guild_id: guildId, + guild_scheduled_event_id: id, + }).pipe( + Effect.andThen(() => { + totalDeleted++; + }), + Effect.catch(() => { + totalFailed++; + return Console.log( + ` ${RED}Failed to delete scheduled event ${id}${RESET}`, + ); + }), + ); + } + } + }); + +const nukeAutoModRules = ( + dryRun: boolean, + nukeConfig: NukeConfig, + guildId: string, +) => + Effect.gen(function* () { + const rules = yield* listAutoModerationRules({ guild_id: guildId }).pipe( + Effect.catch(() => + Console.log( + ` ${RED}Failed to list auto-mod rules for ${guildId}${RESET}`, + ).pipe( + Effect.map( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + () => [] as any[], + ), + ), + ), + ); + + for (const rule of rules) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const r = rule as any; + const id = String(r.id ?? ""); + const name = String(r.name ?? ""); + if (!id) continue; + + totalFound++; + const excluded = isExcluded(nukeConfig, "AutoModRule", id, name); + if (excluded) { + totalSkipped++; + yield* Console.log( + ` ${YELLOW}[SKIP]${RESET} AutoModRule: ${name} ${DIM}(${id})${RESET} — ${excluded.reason ?? "excluded"}`, + ); + continue; + } + + if (dryRun) { + yield* Console.log( + ` ${RED}[DELETE]${RESET} AutoModRule: ${name} ${DIM}(${id})${RESET}`, + ); + } else { + yield* Console.log( + ` ${RED}[DELETE]${RESET} AutoModRule: ${name} ${DIM}(${id})${RESET}`, + ); + yield* deleteAutoModerationRule({ + guild_id: guildId, + rule_id: id, + }).pipe( + Effect.andThen(() => { + totalDeleted++; + }), + Effect.catch(() => { + totalFailed++; + return Console.log( + ` ${RED}Failed to delete auto-mod rule ${id}${RESET}`, + ); + }), + ); + } + } + }); + +const nukeIntegrations = ( + dryRun: boolean, + nukeConfig: NukeConfig, + guildId: string, + selfUserId: string | undefined, +) => + Effect.gen(function* () { + const integrations = yield* listGuildIntegrations({ + guild_id: guildId, + }).pipe( + Effect.catch(() => + Console.log( + ` ${RED}Failed to list integrations for ${guildId}${RESET}`, + ).pipe( + Effect.map( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + () => [] as any[], + ), + ), + ), + ); + + for (const integration of integrations) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const i = integration as any; + const id = String(i.id ?? ""); + const name = String(i.name ?? ""); + const integrationType = String(i.type ?? ""); + const appBotId = i.application?.bot?.id; + if (!id) continue; + + // Never remove our own bot integration — that would kick the bot. + if (selfUserId && appBotId && String(appBotId) === selfUserId) { + yield* Console.log( + ` ${DIM}[SKIP]${RESET} Integration: ${name} ${DIM}(${id}, type: ${integrationType}) — self bot${RESET}`, + ); + continue; + } + + totalFound++; + const excluded = isExcluded(nukeConfig, "Integration", id, name); + if (excluded) { + totalSkipped++; + yield* Console.log( + ` ${YELLOW}[SKIP]${RESET} Integration: ${name} ${DIM}(${id}, type: ${integrationType})${RESET} — ${excluded.reason ?? "excluded"}`, + ); + continue; + } + + if (dryRun) { + yield* Console.log( + ` ${RED}[DELETE]${RESET} Integration: ${name} ${DIM}(${id}, type: ${integrationType})${RESET}`, + ); + } else { + yield* Console.log( + ` ${RED}[DELETE]${RESET} Integration: ${name} ${DIM}(${id}, type: ${integrationType})${RESET}`, + ); + yield* deleteGuildIntegration({ + guild_id: guildId, + integration_id: id, + }).pipe( + Effect.andThen(() => { + totalDeleted++; + }), + Effect.catch(() => { + totalFailed++; + return Console.log( + ` ${RED}Failed to delete integration ${id}${RESET}`, + ); + }), + ); + } + } + }); + +const nukeSoundboardSounds = ( + dryRun: boolean, + nukeConfig: NukeConfig, + guildId: string, +) => + Effect.gen(function* () { + const result = yield* listGuildSoundboardSounds({ + guild_id: guildId, + }).pipe( + Effect.catch(() => + Console.log( + ` ${RED}Failed to list soundboard sounds for ${guildId}${RESET}`, + ).pipe( + Effect.map( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + () => ({ items: [] as any[] }), + ), + ), + ), + ); + + for (const sound of result.items ?? []) { + totalFound++; + const excluded = isExcluded( + nukeConfig, + "SoundboardSound", + sound.sound_id, + sound.name, + ); + if (excluded) { + totalSkipped++; + yield* Console.log( + ` ${YELLOW}[SKIP]${RESET} SoundboardSound: ${sound.name} ${DIM}(${sound.sound_id})${RESET} — ${excluded.reason ?? "excluded"}`, + ); + continue; + } + + if (dryRun) { + yield* Console.log( + ` ${RED}[DELETE]${RESET} SoundboardSound: ${sound.name} ${DIM}(${sound.sound_id})${RESET}`, + ); + } else { + yield* Console.log( + ` ${RED}[DELETE]${RESET} SoundboardSound: ${sound.name} ${DIM}(${sound.sound_id})${RESET}`, + ); + yield* deleteGuildSoundboardSound({ + guild_id: guildId, + sound_id: sound.sound_id, + }).pipe( + Effect.andThen(() => { + totalDeleted++; + }), + Effect.catch(() => { + totalFailed++; + return Console.log( + ` ${RED}Failed to delete soundboard sound ${sound.sound_id}${RESET}`, + ); + }), + ); + } + } + }); + +const nukeGuildEmojis = ( + dryRun: boolean, + nukeConfig: NukeConfig, + guildId: string, +) => + Effect.gen(function* () { + const emojis = yield* listGuildEmojis({ guild_id: guildId }).pipe( + Effect.catch(() => + Console.log( + ` ${RED}Failed to list emojis for ${guildId}${RESET}`, + ).pipe( + Effect.map( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + () => [] as any[], + ), + ), + ), + ); + + for (const emoji of emojis) { + // Skip managed emojis (owned by integrations) + if (emoji.managed) continue; + // Discord allows null id only for unicode emoji which can't be deleted. + if (!emoji.id) continue; + + totalFound++; + const excluded = isExcluded(nukeConfig, "GuildEmoji", emoji.id, emoji.name); + if (excluded) { + totalSkipped++; + yield* Console.log( + ` ${YELLOW}[SKIP]${RESET} GuildEmoji: ${emoji.name} ${DIM}(${emoji.id})${RESET} — ${excluded.reason ?? "excluded"}`, + ); + continue; + } + + if (dryRun) { + yield* Console.log( + ` ${RED}[DELETE]${RESET} GuildEmoji: ${emoji.name} ${DIM}(${emoji.id})${RESET}`, + ); + } else { + yield* Console.log( + ` ${RED}[DELETE]${RESET} GuildEmoji: ${emoji.name} ${DIM}(${emoji.id})${RESET}`, + ); + yield* deleteGuildEmoji({ + guild_id: guildId, + emoji_id: emoji.id, + }).pipe( + Effect.andThen(() => { + totalDeleted++; + }), + Effect.catch(() => { + totalFailed++; + return Console.log( + ` ${RED}Failed to delete emoji ${emoji.id}${RESET}`, + ); + }), + ); + } + } + }); + +const nukeGuildStickers = ( + dryRun: boolean, + nukeConfig: NukeConfig, + guildId: string, +) => + Effect.gen(function* () { + const stickers = yield* listGuildStickers({ guild_id: guildId }).pipe( + Effect.catch(() => + Console.log( + ` ${RED}Failed to list stickers for ${guildId}${RESET}`, + ).pipe( + Effect.map( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + () => [] as any[], + ), + ), + ), + ); + + for (const sticker of stickers) { + totalFound++; + const excluded = isExcluded( + nukeConfig, + "GuildSticker", + sticker.id, + sticker.name, + ); + if (excluded) { + totalSkipped++; + yield* Console.log( + ` ${YELLOW}[SKIP]${RESET} GuildSticker: ${sticker.name} ${DIM}(${sticker.id})${RESET} — ${excluded.reason ?? "excluded"}`, + ); + continue; + } + + if (dryRun) { + yield* Console.log( + ` ${RED}[DELETE]${RESET} GuildSticker: ${sticker.name} ${DIM}(${sticker.id})${RESET}`, + ); + } else { + yield* Console.log( + ` ${RED}[DELETE]${RESET} GuildSticker: ${sticker.name} ${DIM}(${sticker.id})${RESET}`, + ); + yield* deleteGuildSticker({ + guild_id: guildId, + sticker_id: sticker.id, + }).pipe( + Effect.andThen(() => { + totalDeleted++; + }), + Effect.catch(() => { + totalFailed++; + return Console.log( + ` ${RED}Failed to delete sticker ${sticker.id}${RESET}`, + ); + }), + ); + } + } + }); + +const nukeGuildTemplates = ( + dryRun: boolean, + nukeConfig: NukeConfig, + guildId: string, +) => + Effect.gen(function* () { + const templates = yield* listGuildTemplates({ guild_id: guildId }).pipe( + Effect.catch(() => + Console.log( + ` ${RED}Failed to list templates for ${guildId}${RESET}`, + ).pipe( + Effect.map( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + () => [] as any[], + ), + ), + ), + ); + + for (const tpl of templates) { + totalFound++; + const excluded = isExcluded(nukeConfig, "GuildTemplate", tpl.code, tpl.name); + if (excluded) { + totalSkipped++; + yield* Console.log( + ` ${YELLOW}[SKIP]${RESET} GuildTemplate: ${tpl.name} ${DIM}(${tpl.code})${RESET} — ${excluded.reason ?? "excluded"}`, + ); + continue; + } + + if (dryRun) { + yield* Console.log( + ` ${RED}[DELETE]${RESET} GuildTemplate: ${tpl.name} ${DIM}(${tpl.code})${RESET}`, + ); + } else { + yield* Console.log( + ` ${RED}[DELETE]${RESET} GuildTemplate: ${tpl.name} ${DIM}(${tpl.code})${RESET}`, + ); + yield* deleteGuildTemplate({ + guild_id: guildId, + code: tpl.code, + }).pipe( + Effect.andThen(() => { + totalDeleted++; + }), + Effect.catch(() => { + totalFailed++; + return Console.log( + ` ${RED}Failed to delete template ${tpl.code}${RESET}`, + ); + }), + ); + } + } + }); + +const nukeGuildWebhooks = ( + dryRun: boolean, + nukeConfig: NukeConfig, + guildId: string, + applicationId: string, +) => + Effect.gen(function* () { + const hooks = yield* getGuildWebhooks({ guild_id: guildId }).pipe( + Effect.catch(() => + Console.log( + ` ${RED}Failed to list webhooks for ${guildId}${RESET}`, + ).pipe( + Effect.map( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + () => [] as any[], + ), + ), + ), + ); + + for (const webhook of hooks) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const w = webhook as any; + const id = String(w.id ?? ""); + const name = String(w.name ?? ""); + const ownerAppId = w.application_id ? String(w.application_id) : null; + if (!id) continue; + + // Only delete webhooks owned by this application — touching others would + // disrupt unrelated integrations. + if (ownerAppId !== applicationId) { + yield* Console.log( + ` ${DIM}[SKIP]${RESET} Webhook: ${name} ${DIM}(${id}, owner: ${ownerAppId ?? "user"}) — not owned by this app${RESET}`, + ); + continue; + } + + totalFound++; + const excluded = isExcluded(nukeConfig, "Webhook", id, name); + if (excluded) { + totalSkipped++; + yield* Console.log( + ` ${YELLOW}[SKIP]${RESET} Webhook: ${name} ${DIM}(${id})${RESET} — ${excluded.reason ?? "excluded"}`, + ); + continue; + } + + if (dryRun) { + yield* Console.log( + ` ${RED}[DELETE]${RESET} Webhook: ${name} ${DIM}(${id})${RESET}`, + ); + } else { + yield* Console.log( + ` ${RED}[DELETE]${RESET} Webhook: ${name} ${DIM}(${id})${RESET}`, + ); + yield* deleteWebhook({ webhook_id: id }).pipe( + Effect.andThen(() => { + totalDeleted++; + }), + Effect.catch(() => { + totalFailed++; + return Console.log( + ` ${RED}Failed to delete webhook ${id}${RESET}`, + ); + }), + ); + } + } + }); + +const nukeGuildChannels = ( + dryRun: boolean, + nukeConfig: NukeConfig, + guildId: string, +) => + Effect.gen(function* () { + const channels = yield* listGuildChannels({ guild_id: guildId }).pipe( + Effect.catch(() => + Console.log( + ` ${RED}Failed to list channels for ${guildId}${RESET}`, + ).pipe( + Effect.map( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + () => [] as any[], + ), + ), + ), + ); + + // Sort: leaf channels first, categories last (categories have type 4) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sorted = [...channels].sort((a: any, b: any) => { + const aIsCat = a?.type === 4 ? 1 : 0; + const bIsCat = b?.type === 4 ? 1 : 0; + return aIsCat - bIsCat; + }); + + for (const channel of sorted) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const c = channel as any; + const id = String(c.id ?? ""); + const name = String(c.name ?? ""); + const type = c.type; + if (!id) continue; + + totalFound++; + const excluded = isExcluded(nukeConfig, "Channel", id, name); + if (excluded) { + totalSkipped++; + yield* Console.log( + ` ${YELLOW}[SKIP]${RESET} Channel: ${name} ${DIM}(${id}, type: ${type})${RESET} — ${excluded.reason ?? "excluded"}`, + ); + continue; + } + + if (dryRun) { + yield* Console.log( + ` ${RED}[DELETE]${RESET} Channel: ${name} ${DIM}(${id}, type: ${type})${RESET}`, + ); + } else { + yield* Console.log( + ` ${RED}[DELETE]${RESET} Channel: ${name} ${DIM}(${id}, type: ${type})${RESET}`, + ); + yield* deleteChannel({ channel_id: id }).pipe( + Effect.andThen(() => { + totalDeleted++; + }), + Effect.catch(() => { + totalFailed++; + return Console.log( + ` ${RED}Failed to delete channel ${id}${RESET}`, + ); + }), + ); + } + } + }); + +const nukeGuildRoles = ( + dryRun: boolean, + nukeConfig: NukeConfig, + guildId: string, +) => + Effect.gen(function* () { + const roles = yield* listGuildRoles({ guild_id: guildId }).pipe( + Effect.catch(() => + Console.log( + ` ${RED}Failed to list roles for ${guildId}${RESET}`, + ).pipe( + Effect.map( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + () => [] as any[], + ), + ), + ), + ); + + for (const role of roles) { + // The @everyone role has the same id as the guild and cannot be deleted + if (role.id === guildId) continue; + // Managed roles are owned by integrations and cannot be deleted directly + if (role.managed) continue; + + totalFound++; + const excluded = isExcluded(nukeConfig, "Role", role.id, role.name); + if (excluded) { + totalSkipped++; + yield* Console.log( + ` ${YELLOW}[SKIP]${RESET} Role: ${role.name} ${DIM}(${role.id})${RESET} — ${excluded.reason ?? "excluded"}`, + ); + continue; + } + + if (dryRun) { + yield* Console.log( + ` ${RED}[DELETE]${RESET} Role: ${role.name} ${DIM}(${role.id})${RESET}`, + ); + } else { + yield* Console.log( + ` ${RED}[DELETE]${RESET} Role: ${role.name} ${DIM}(${role.id})${RESET}`, + ); + yield* deleteGuildRole({ + guild_id: guildId, + role_id: role.id, + }).pipe( + Effect.andThen(() => { + totalDeleted++; + }), + Effect.catch(() => { + totalFailed++; + return Console.log( + ` ${RED}Failed to delete role ${role.id}${RESET}`, + ); + }), + ); + } + } + }); + +const nukeGuilds = ( + dryRun: boolean, + nukeConfig: NukeConfig, + applicationId: string, + selfUserId: string | undefined, +) => + Effect.gen(function* () { + yield* Console.log(`\n${BOLD}${CYAN}Guilds${RESET}`); + + const guilds = yield* listMyGuilds({ limit: 200 }).pipe( + Effect.catch(() => + Console.log(` ${RED}Failed to list guilds${RESET}`).pipe( + Effect.map( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + () => [] as any[], + ), + ), + ), + ); + + if (guilds.length === 0) { + yield* Console.log(` ${DIM}No guilds found${RESET}`); + return; + } + + for (const guild of guilds) { + yield* Console.log( + `\n ${BOLD}Guild: ${guild.name}${RESET} ${DIM}(${guild.id}, owner: ${guild.owner})${RESET}`, + ); + + const guildExcluded = isExcluded( + nukeConfig, + "Guild", + guild.id, + guild.name, + ); + const effectiveDryRun = dryRun || !!guildExcluded; + if (guildExcluded) { + yield* Console.log( + ` ${YELLOW}[SKIP]${RESET} Guild: ${guild.name} ${DIM}(${guild.id})${RESET} — ${guildExcluded.reason ?? "excluded"} (children listed but not deleted)`, + ); + } + + // Delete child resources in dependency order. Roles/channels last so + // earlier ops still have permission and references resolve. + yield* nukeGuildCommands( + effectiveDryRun, + nukeConfig, + applicationId, + guild.id, + ); + yield* nukeScheduledEvents(effectiveDryRun, nukeConfig, guild.id); + yield* nukeAutoModRules(effectiveDryRun, nukeConfig, guild.id); + yield* nukeIntegrations( + effectiveDryRun, + nukeConfig, + guild.id, + selfUserId, + ); + yield* nukeSoundboardSounds(effectiveDryRun, nukeConfig, guild.id); + yield* nukeGuildEmojis(effectiveDryRun, nukeConfig, guild.id); + yield* nukeGuildStickers(effectiveDryRun, nukeConfig, guild.id); + yield* nukeGuildTemplates(effectiveDryRun, nukeConfig, guild.id); + yield* nukeGuildWebhooks( + effectiveDryRun, + nukeConfig, + guild.id, + applicationId, + ); + yield* nukeGuildChannels(effectiveDryRun, nukeConfig, guild.id); + yield* nukeGuildRoles(effectiveDryRun, nukeConfig, guild.id); + } + }); + +// ============================================================================ +// Main command +// ============================================================================ + +const nuke = Command.make( + "nuke", + { + dryRun: Flag.boolean("dry-run").pipe( + Flag.withDescription("Only list resources without deleting them"), + Flag.withDefault(false), + ), + }, + (config) => + Effect.gen(function* () { + const nukeConfig = loadNukeConfig(); + const mode = config.dryRun + ? `${YELLOW}DRY RUN${RESET}` + : `${RED}LIVE${RESET}`; + yield* Console.log( + `\n${BOLD}Discord Nuke${RESET} ${DIM}(${mode}${DIM})${RESET}`, + ); + + if (!config.dryRun) { + yield* Console.log( + `${RED}${BOLD}WARNING: This will DELETE bot-owned resources across every guild the bot is a member of!${RESET}`, + ); + } + + if (nukeConfig.exclude && nukeConfig.exclude.length > 0) { + yield* Console.log( + `${DIM}Loaded ${nukeConfig.exclude.length} exclusion rule(s) from nuke-config.json${RESET}`, + ); + } + + // Resolve the application id (and self bot id, where available). + const app = yield* getMyApplication({}).pipe( + Effect.catch((err) => { + console.error( + `${RED}Failed to resolve current application via /applications/@me${RESET}`, + ); + return Effect.fail(err); + }), + ); + + const applicationId = app.id; + const selfUserId = app.bot?.id; + + yield* Console.log( + `${DIM}Application: ${app.name} (${applicationId}); bot user id: ${selfUserId ?? ""}${RESET}`, + ); + + // 1. Application-level resources + yield* nukeGlobalCommands(config.dryRun, nukeConfig, applicationId); + yield* nukeApplicationEmojis(config.dryRun, nukeConfig, applicationId); + + // 2. Guild-level resources (per guild the bot is in) + yield* nukeGuilds(config.dryRun, nukeConfig, applicationId, selfUserId); + + // Summary + yield* Console.log(`\n${BOLD}Summary${RESET}`); + yield* Console.log(` Total found: ${totalFound}`); + yield* Console.log( + ` ${YELLOW}Skipped: ${totalSkipped}${RESET}`, + ); + if (!config.dryRun) { + yield* Console.log( + ` ${GREEN}Deleted: ${totalDeleted}${RESET}`, + ); + if (totalFailed > 0) { + yield* Console.log( + ` ${RED}Failed: ${totalFailed}${RESET}`, + ); + } + } + }).pipe( + Effect.provide(CredentialsFromEnv), + Effect.provide(FetchHttpClient.layer), + ), +).pipe( + Command.withDescription( + "List and delete all Discord resources owned by the authenticated bot/application", + ), +); + +// ============================================================================ +// Entry Point +// ============================================================================ + +BunRuntime.runMain( + Effect.provide(Command.run(nuke, { version: "1.0.0" }), BunServices.layer), +); diff --git a/packages/discord/src/category.ts b/packages/discord/src/category.ts new file mode 100644 index 000000000..a09ac2ecc --- /dev/null +++ b/packages/discord/src/category.ts @@ -0,0 +1,4 @@ +/** + * Re-export the shared category system from sdk-core. + */ +export * from "@distilled.cloud/core/category"; diff --git a/packages/discord/src/client.ts b/packages/discord/src/client.ts new file mode 100644 index 000000000..850333451 --- /dev/null +++ b/packages/discord/src/client.ts @@ -0,0 +1,132 @@ +/** + * Discord API Client. + * + * Wraps the shared REST client from sdk-core with Discord-specific + * error matching and credential handling. + */ +import * as Duration from "effect/Duration"; +import * as Effect from "effect/Effect"; +import * as Redacted from "effect/Redacted"; +import * as Schema from "effect/Schema"; +import { makeAPI } from "@distilled.cloud/core/client"; +import { parseRetryAfterForStatus } from "@distilled.cloud/core/retry-after"; +import { Retry } from "./retry.ts"; +import { + HTTP_STATUS_MAP, + UnknownDiscordError, + DiscordParseError, + DiscordRateLimited, + MethodNotAllowed, +} from "./errors.ts"; + +// Re-export for backwards compatibility +export { UnknownDiscordError } from "./errors.ts"; +import { Credentials } from "./credentials.ts"; + +/** + * Discord error response shape. + * + * All Discord API errors are returned as JSON with at minimum + * `{ code: number, message: string }`. Validation errors and rate-limit + * responses extend this with additional fields. + * + * See: https://discord.com/developers/docs/reference#error-messages + */ +const ApiErrorResponse = Schema.Struct({ + code: Schema.optional(Schema.Number), + message: Schema.optional(Schema.String), + errors: Schema.optional(Schema.Unknown), + // Ratelimit-only fields: + retry_after: Schema.optional(Schema.Number), + global: Schema.optional(Schema.Boolean), +}); + +/** + * Match a Discord API error response to the appropriate error class based + * on HTTP status. Discord returns a `code` (an integer error code, not the + * HTTP status) plus a human-readable `message`. Rate-limit responses (429) + * also include `retry_after` (in seconds) and `global`. + */ +const matchError = ( + status: number, + errorBody: unknown, + _errors?: readonly unknown[], + headers?: Record, +): Effect.Effect => { + let parsed: + | { + code?: number; + message?: string; + errors?: unknown; + retry_after?: number; + global?: boolean; + } + | undefined; + + try { + parsed = Schema.decodeUnknownSync(ApiErrorResponse)(errorBody); + } catch { + return Effect.fail(new UnknownDiscordError({ body: errorBody })); + } + + const message = parsed.message ?? ""; + const code = parsed.code !== undefined ? String(parsed.code) : undefined; + + if (status === 405) { + return Effect.fail( + new MethodNotAllowed({ + message: message || "405: Method Not Allowed", + }), + ); + } + + if (status === 429) { + // Prefer the body's retry_after (Discord reports it in seconds), then fall + // back to the standard Retry-After / RateLimit-Reset-After headers. + const retryAfter = + parsed.retry_after !== undefined + ? Duration.seconds(parsed.retry_after) + : parseRetryAfterForStatus(status, headers); + return Effect.fail( + new DiscordRateLimited({ + message, + code, + retryAfter, + global: parsed.global, + errors: parsed.errors, + }), + ); + } + + const ErrorClass = (HTTP_STATUS_MAP as any)[status]; + if (ErrorClass) { + return Effect.fail( + new ErrorClass({ + message, + retryAfter: parseRetryAfterForStatus(status, headers), + }), + ); + } + + return Effect.fail( + new UnknownDiscordError({ + code, + message: parsed.message, + body: errorBody, + }), + ); +}; + +/** + * Discord API client. + */ +export const API = makeAPI({ + credentials: Credentials as any, + getBaseUrl: (creds: any) => creds.apiBaseUrl, + getAuthHeaders: (creds: any) => ({ + Authorization: `${creds.authScheme} ${Redacted.value(creds.token)}`, + }), + matchError, + ParseError: DiscordParseError as any, + retry: Retry as any, +}); diff --git a/packages/discord/src/credentials.ts b/packages/discord/src/credentials.ts new file mode 100644 index 000000000..d29b65d89 --- /dev/null +++ b/packages/discord/src/credentials.ts @@ -0,0 +1,64 @@ +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Redacted from "effect/Redacted"; +import * as Context from "effect/Context"; +import { ConfigError } from "@distilled.cloud/core/errors"; + +export const DEFAULT_API_BASE_URL = "https://discord.com/api/v10"; + +/** + * Discord auth scheme. + * + * - `"Bot"` — bot tokens (`Authorization: Bot `). Default. + * - `"Bearer"` — OAuth2 bearer tokens (`Authorization: Bearer `). + */ +export type AuthScheme = "Bot" | "Bearer"; + +export interface Config { + readonly token: Redacted.Redacted; + readonly authScheme: AuthScheme; + readonly apiBaseUrl: string; +} + +export class Credentials extends Context.Service()( + "DiscordCredentials", +) {} + +/** + * Load credentials from environment variables. + * + * - `DISCORD_BOT_TOKEN` (preferred) — bot token, used with `Authorization: Bot `. + * - `DISCORD_TOKEN` — alias for `DISCORD_BOT_TOKEN`. + * - `DISCORD_BEARER_TOKEN` — OAuth2 bearer token, used with `Authorization: Bearer `. + * If both are set, `DISCORD_BEARER_TOKEN` takes precedence. + * - `DISCORD_API_BASE_URL` — overrides the default base URL (optional). + */ +export const CredentialsFromEnv = Layer.effect( + Credentials, + Effect.gen(function* () { + const bearer = process.env.DISCORD_BEARER_TOKEN; + const bot = process.env.DISCORD_BOT_TOKEN ?? process.env.DISCORD_TOKEN; + const apiBaseUrl = process.env.DISCORD_API_BASE_URL ?? DEFAULT_API_BASE_URL; + + if (bearer) { + return { + token: Redacted.make(bearer), + authScheme: "Bearer" as const, + apiBaseUrl, + }; + } + + if (bot) { + return { + token: Redacted.make(bot), + authScheme: "Bot" as const, + apiBaseUrl, + }; + } + + return yield* new ConfigError({ + message: + "DISCORD_BOT_TOKEN (or DISCORD_TOKEN) or DISCORD_BEARER_TOKEN environment variable is required", + }); + }), +); diff --git a/packages/discord/src/errors.ts b/packages/discord/src/errors.ts new file mode 100644 index 000000000..1a20f6167 --- /dev/null +++ b/packages/discord/src/errors.ts @@ -0,0 +1,92 @@ +/** + * Discord-specific error types. + * + * Re-exports common HTTP errors from sdk-core and adds Discord-specific + * error matching and API error types. + */ +export { + BadGateway, + BadRequest, + Conflict, + ConfigError, + Forbidden, + GatewayTimeout, + InternalServerError, + Locked, + NotFound, + ServiceUnavailable, + TooManyRequests, + Unauthorized, + UnprocessableEntity, + HTTP_STATUS_MAP, + DEFAULT_ERRORS, + API_ERRORS, +} from "@distilled.cloud/core/errors"; +export type { DefaultErrors } from "@distilled.cloud/core/errors"; + +import * as Schema from "effect/Schema"; +import * as Category from "@distilled.cloud/core/category"; +import { DurationSchema } from "@distilled.cloud/core/errors"; + +/** + * Discord rate-limit error (HTTP 429). + * + * Discord's 429 response body carries `retry_after` (seconds) and a `global` + * flag indicating whether the limit applies to the whole bot or just the + * route. The numeric `code` is Discord's internal error code, not the HTTP + * status. + * + * See: https://discord.com/developers/docs/topics/rate-limits + */ +export class DiscordRateLimited extends Schema.TaggedErrorClass()( + "DiscordRateLimited", + { + message: Schema.String, + code: Schema.optional(Schema.String), + retryAfter: Schema.optional(DurationSchema), + global: Schema.optional(Schema.Boolean), + errors: Schema.optional(Schema.Unknown), + }, +).pipe( + Category.withThrottlingError, + Category.withRetryable({ throttling: true }), +) {} + +/** + * MethodNotAllowed - HTTP method not allowed on the route (405). + * + * Discord's edge layer returns a plain `{"message": "405: Method Not Allowed", + * "code": 0}` body for requests sent with an HTTP method that the route does + * not accept (e.g. PATCH on `/gateway`, DELETE on `/gateway/bot`). This is not + * in the shared `HTTP_STATUS_MAP`, so without an explicit class these surface + * as `UnknownDiscordError`. + */ +export class MethodNotAllowed extends Schema.TaggedErrorClass()( + "MethodNotAllowed", + { message: Schema.String }, +).pipe(Category.withBadRequestError) {} + +/** + * Unknown Discord error — returned when an error response doesn't map to a + * standard HTTP status class. The `code` field holds Discord's internal + * numeric error code (stringified). + * + * See: https://discord.com/developers/docs/topics/opcodes-and-status-codes#json + */ +export class UnknownDiscordError extends Schema.TaggedErrorClass()( + "UnknownDiscordError", + { + code: Schema.optional(Schema.String), + message: Schema.optional(Schema.String), + body: Schema.Unknown, + }, +).pipe(Category.withServerError) {} + +// Schema parse error wrapper +export class DiscordParseError extends Schema.TaggedErrorClass()( + "DiscordParseError", + { + body: Schema.Unknown, + cause: Schema.Unknown, + }, +).pipe(Category.withParseError) {} diff --git a/packages/discord/src/index.ts b/packages/discord/src/index.ts new file mode 100644 index 000000000..5e4f3b6e0 --- /dev/null +++ b/packages/discord/src/index.ts @@ -0,0 +1,15 @@ +/** + * Discord SDK for Effect + * + * @example + * \`\`\`ts + * import * as Discord from "@distilled.cloud/discord"; + * \`\`\` + */ +export * from "./credentials.ts"; +export * as Category from "./category.ts"; +export * as T from "./traits.ts"; +export * as Retry from "./retry.ts"; +export { API } from "./client.ts"; +export * from "./errors.ts"; +export { SensitiveString, SensitiveNullableString } from "./sensitive.ts"; diff --git a/packages/discord/src/operations/actionGuildJoinRequest.ts b/packages/discord/src/operations/actionGuildJoinRequest.ts new file mode 100644 index 000000000..941542a86 --- /dev/null +++ b/packages/discord/src/operations/actionGuildJoinRequest.ts @@ -0,0 +1,51 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ActionGuildJoinRequestInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + request_id: Schema.String.pipe(T.PathParam()), + action: Schema.optional(Schema.Unknown), + rejection_reason: Schema.optional(Schema.NullOr(Schema.String)), + }).pipe( + T.Http({ + method: "PATCH", + path: "/guilds/{guild_id}/requests/{request_id}", + }), + ); +export type ActionGuildJoinRequestInput = + typeof ActionGuildJoinRequestInput.Type; + +// Output Schema +export const ActionGuildJoinRequestOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + created_at: Schema.String, + reviewed_at: Schema.NullOr(Schema.String), + application_status: Schema.Unknown, + rejection_reason: Schema.NullOr(Schema.String), + guild_id: Schema.String, + user_id: Schema.String, + user: Schema.optional(Schema.Unknown), + form_responses: Schema.optional( + Schema.NullOr(Schema.Array(Schema.Unknown)), + ), + actioned_by_user: Schema.optional(Schema.Unknown), + }); +export type ActionGuildJoinRequestOutput = + typeof ActionGuildJoinRequestOutput.Type; + +// The operation +/** + * Approve or reject guild join request + */ +export const actionGuildJoinRequest = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: ActionGuildJoinRequestInput, + outputSchema: ActionGuildJoinRequestOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/addGroupDmUser.ts b/packages/discord/src/operations/addGroupDmUser.ts new file mode 100644 index 000000000..5ee57ee90 --- /dev/null +++ b/packages/discord/src/operations/addGroupDmUser.ts @@ -0,0 +1,30 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; +import { SensitiveNullableString } from "../sensitive.ts"; + +// Input Schema +export const AddGroupDmUserInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + user_id: Schema.String.pipe(T.PathParam()), + access_token: Schema.optional(SensitiveNullableString), + nick: Schema.optional(Schema.NullOr(Schema.String)), +}).pipe( + T.Http({ + method: "PUT", + path: "/channels/{channel_id}/recipients/{user_id}", + }), +); +export type AddGroupDmUserInput = typeof AddGroupDmUserInput.Type; + +// Output Schema +export const AddGroupDmUserOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Unknown; +export type AddGroupDmUserOutput = typeof AddGroupDmUserOutput.Type; + +// The operation +export const addGroupDmUser = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: AddGroupDmUserInput, + outputSchema: AddGroupDmUserOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/addGuildMember.ts b/packages/discord/src/operations/addGuildMember.ts new file mode 100644 index 000000000..eee5dedb1 --- /dev/null +++ b/packages/discord/src/operations/addGuildMember.ts @@ -0,0 +1,61 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; +import { SensitiveString } from "../sensitive.ts"; + +// Input Schema +export const AddGuildMemberInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + user_id: Schema.String.pipe(T.PathParam()), + nick: Schema.optional(Schema.NullOr(Schema.String)), + roles: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + mute: Schema.optional(Schema.NullOr(Schema.Boolean)), + deaf: Schema.optional(Schema.NullOr(Schema.Boolean)), + access_token: SensitiveString, + flags: Schema.optional(Schema.NullOr(Schema.Number)), +}).pipe( + T.Http({ method: "PUT", path: "/guilds/{guild_id}/members/{user_id}" }), +); +export type AddGuildMemberInput = typeof AddGuildMemberInput.Type; + +// Output Schema +export const AddGuildMemberOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, +}); +export type AddGuildMemberOutput = typeof AddGuildMemberOutput.Type; + +// The operation +export const addGuildMember = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: AddGuildMemberInput, + outputSchema: AddGuildMemberOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/addGuildMemberRole.ts b/packages/discord/src/operations/addGuildMemberRole.ts new file mode 100644 index 000000000..5fc3fdf90 --- /dev/null +++ b/packages/discord/src/operations/addGuildMemberRole.ts @@ -0,0 +1,29 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const AddGuildMemberRoleInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + user_id: Schema.String.pipe(T.PathParam()), + role_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "PUT", + path: "/guilds/{guild_id}/members/{user_id}/roles/{role_id}", + }), + ); +export type AddGuildMemberRoleInput = typeof AddGuildMemberRoleInput.Type; + +// Output Schema +export const AddGuildMemberRoleOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type AddGuildMemberRoleOutput = typeof AddGuildMemberRoleOutput.Type; + +// The operation +export const addGuildMemberRole = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: AddGuildMemberRoleInput, + outputSchema: AddGuildMemberRoleOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/addLobbyMember.ts b/packages/discord/src/operations/addLobbyMember.ts new file mode 100644 index 000000000..e66278b07 --- /dev/null +++ b/packages/discord/src/operations/addLobbyMember.ts @@ -0,0 +1,32 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const AddLobbyMemberInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + lobby_id: Schema.String.pipe(T.PathParam()), + user_id: Schema.String.pipe(T.PathParam()), + metadata: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + flags: Schema.optional(Schema.Unknown), +}).pipe( + T.Http({ method: "PUT", path: "/lobbies/{lobby_id}/members/{user_id}" }), +); +export type AddLobbyMemberInput = typeof AddLobbyMemberInput.Type; + +// Output Schema +export const AddLobbyMemberOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + metadata: Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + flags: Schema.Number, +}); +export type AddLobbyMemberOutput = typeof AddLobbyMemberOutput.Type; + +// The operation +export const addLobbyMember = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: AddLobbyMemberInput, + outputSchema: AddLobbyMemberOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/addMyMessageReaction.ts b/packages/discord/src/operations/addMyMessageReaction.ts new file mode 100644 index 000000000..7a4b689e0 --- /dev/null +++ b/packages/discord/src/operations/addMyMessageReaction.ts @@ -0,0 +1,32 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const AddMyMessageReactionInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), + emoji_name: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "PUT", + path: "/channels/{channel_id}/messages/{message_id}/reactions/{emoji_name}/@me", + }), + ); +export type AddMyMessageReactionInput = typeof AddMyMessageReactionInput.Type; + +// Output Schema +export const AddMyMessageReactionOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type AddMyMessageReactionOutput = typeof AddMyMessageReactionOutput.Type; + +// The operation +export const addMyMessageReaction = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: AddMyMessageReactionInput, + outputSchema: AddMyMessageReactionOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/addThreadMember.ts b/packages/discord/src/operations/addThreadMember.ts new file mode 100644 index 000000000..4f9bf1de0 --- /dev/null +++ b/packages/discord/src/operations/addThreadMember.ts @@ -0,0 +1,27 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const AddThreadMemberInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + user_id: Schema.String.pipe(T.PathParam()), +}).pipe( + T.Http({ + method: "PUT", + path: "/channels/{channel_id}/thread-members/{user_id}", + }), +); +export type AddThreadMemberInput = typeof AddThreadMemberInput.Type; + +// Output Schema +export const AddThreadMemberOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type AddThreadMemberOutput = typeof AddThreadMemberOutput.Type; + +// The operation +export const addThreadMember = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: AddThreadMemberInput, + outputSchema: AddThreadMemberOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/applicationsGetActivityInstance.ts b/packages/discord/src/operations/applicationsGetActivityInstance.ts new file mode 100644 index 000000000..5615c34cc --- /dev/null +++ b/packages/discord/src/operations/applicationsGetActivityInstance.ts @@ -0,0 +1,38 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ApplicationsGetActivityInstanceInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + instance_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "GET", + path: "/applications/{application_id}/activity-instances/{instance_id}", + }), + ); +export type ApplicationsGetActivityInstanceInput = + typeof ApplicationsGetActivityInstanceInput.Type; + +// Output Schema +export const ApplicationsGetActivityInstanceOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String, + instance_id: Schema.String, + launch_id: Schema.String, + location: Schema.Unknown, + users: Schema.Array(Schema.String), + }); +export type ApplicationsGetActivityInstanceOutput = + typeof ApplicationsGetActivityInstanceOutput.Type; + +// The operation +export const applicationsGetActivityInstance = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ApplicationsGetActivityInstanceInput, + outputSchema: ApplicationsGetActivityInstanceOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/banUserFromGuild.ts b/packages/discord/src/operations/banUserFromGuild.ts new file mode 100644 index 000000000..b27ae04b0 --- /dev/null +++ b/packages/discord/src/operations/banUserFromGuild.ts @@ -0,0 +1,24 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const BanUserFromGuildInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + user_id: Schema.String.pipe(T.PathParam()), + delete_message_seconds: Schema.optional(Schema.NullOr(Schema.Number)), + delete_message_days: Schema.optional(Schema.NullOr(Schema.Number)), +}).pipe(T.Http({ method: "PUT", path: "/guilds/{guild_id}/bans/{user_id}" })); +export type BanUserFromGuildInput = typeof BanUserFromGuildInput.Type; + +// Output Schema +export const BanUserFromGuildOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type BanUserFromGuildOutput = typeof BanUserFromGuildOutput.Type; + +// The operation +export const banUserFromGuild = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: BanUserFromGuildInput, + outputSchema: BanUserFromGuildOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/botPartnerSdkToken.ts b/packages/discord/src/operations/botPartnerSdkToken.ts new file mode 100644 index 000000000..2ac48a05a --- /dev/null +++ b/packages/discord/src/operations/botPartnerSdkToken.ts @@ -0,0 +1,34 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; +import { SensitiveString, SensitiveNullableString } from "../sensitive.ts"; + +// Input Schema +export const BotPartnerSdkTokenInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + external_user_id: Schema.String, + preferred_global_name: Schema.optional(Schema.NullOr(Schema.String)), + }).pipe(T.Http({ method: "POST", path: "/partner-sdk/token/bot" })); +export type BotPartnerSdkTokenInput = typeof BotPartnerSdkTokenInput.Type; + +// Output Schema +export const BotPartnerSdkTokenOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + token_type: Schema.String, + access_token: SensitiveString, + expires_in: Schema.Number, + scope: Schema.String, + id_token: Schema.String, + refresh_token: Schema.optional(SensitiveNullableString), + scopes: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + expires_at_s: Schema.optional(Schema.NullOr(Schema.Number)), + }); +export type BotPartnerSdkTokenOutput = typeof BotPartnerSdkTokenOutput.Type; + +// The operation +export const botPartnerSdkToken = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: BotPartnerSdkTokenInput, + outputSchema: BotPartnerSdkTokenOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/botPartnerSdkUnmergeProvisionalAccount.ts b/packages/discord/src/operations/botPartnerSdkUnmergeProvisionalAccount.ts new file mode 100644 index 000000000..cd8c348cc --- /dev/null +++ b/packages/discord/src/operations/botPartnerSdkUnmergeProvisionalAccount.ts @@ -0,0 +1,31 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const BotPartnerSdkUnmergeProvisionalAccountInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + external_user_id: Schema.String, + }).pipe( + T.Http({ + method: "POST", + path: "/partner-sdk/provisional-accounts/unmerge/bot", + }), + ); +export type BotPartnerSdkUnmergeProvisionalAccountInput = + typeof BotPartnerSdkUnmergeProvisionalAccountInput.Type; + +// Output Schema +export const BotPartnerSdkUnmergeProvisionalAccountOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type BotPartnerSdkUnmergeProvisionalAccountOutput = + typeof BotPartnerSdkUnmergeProvisionalAccountOutput.Type; + +// The operation +export const botPartnerSdkUnmergeProvisionalAccount = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: BotPartnerSdkUnmergeProvisionalAccountInput, + outputSchema: BotPartnerSdkUnmergeProvisionalAccountOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/bulkBanUsersFromGuild.ts b/packages/discord/src/operations/bulkBanUsersFromGuild.ts new file mode 100644 index 000000000..e2068ab95 --- /dev/null +++ b/packages/discord/src/operations/bulkBanUsersFromGuild.ts @@ -0,0 +1,31 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const BulkBanUsersFromGuildInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + user_ids: Schema.Array(Schema.String), + delete_message_seconds: Schema.optional(Schema.NullOr(Schema.Number)), + }).pipe(T.Http({ method: "POST", path: "/guilds/{guild_id}/bulk-ban" })); +export type BulkBanUsersFromGuildInput = typeof BulkBanUsersFromGuildInput.Type; + +// Output Schema +export const BulkBanUsersFromGuildOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + banned_users: Schema.Array(Schema.String), + failed_users: Schema.Array(Schema.String), + }); +export type BulkBanUsersFromGuildOutput = + typeof BulkBanUsersFromGuildOutput.Type; + +// The operation +export const bulkBanUsersFromGuild = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: BulkBanUsersFromGuildInput, + outputSchema: BulkBanUsersFromGuildOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/bulkDeleteMessages.ts b/packages/discord/src/operations/bulkDeleteMessages.ts new file mode 100644 index 000000000..2c6432b1c --- /dev/null +++ b/packages/discord/src/operations/bulkDeleteMessages.ts @@ -0,0 +1,28 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const BulkDeleteMessagesInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + messages: Schema.Array(Schema.String), + }).pipe( + T.Http({ + method: "POST", + path: "/channels/{channel_id}/messages/bulk-delete", + }), + ); +export type BulkDeleteMessagesInput = typeof BulkDeleteMessagesInput.Type; + +// Output Schema +export const BulkDeleteMessagesOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type BulkDeleteMessagesOutput = typeof BulkDeleteMessagesOutput.Type; + +// The operation +export const bulkDeleteMessages = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: BulkDeleteMessagesInput, + outputSchema: BulkDeleteMessagesOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/bulkSetApplicationCommands.ts b/packages/discord/src/operations/bulkSetApplicationCommands.ts new file mode 100644 index 000000000..da368da04 --- /dev/null +++ b/packages/discord/src/operations/bulkSetApplicationCommands.ts @@ -0,0 +1,53 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const BulkSetApplicationCommandsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ method: "PUT", path: "/applications/{application_id}/commands" }), + ); +export type BulkSetApplicationCommandsInput = + typeof BulkSetApplicationCommandsInput.Type; + +// Output Schema +export const BulkSetApplicationCommandsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + id: Schema.String, + application_id: Schema.String, + version: Schema.String, + default_member_permissions: Schema.NullOr(Schema.String), + type: Schema.Unknown, + name: Schema.String, + name_localized: Schema.optional(Schema.String), + name_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + description: Schema.String, + description_localized: Schema.optional(Schema.String), + description_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + guild_id: Schema.optional(Schema.String), + dm_permission: Schema.optional(Schema.Boolean), + contexts: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + integration_types: Schema.optional(Schema.Array(Schema.Unknown)), + options: Schema.optional(Schema.Array(Schema.Unknown)), + nsfw: Schema.optional(Schema.Boolean), + }), + ); +export type BulkSetApplicationCommandsOutput = + typeof BulkSetApplicationCommandsOutput.Type; + +// The operation +export const bulkSetApplicationCommands = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: BulkSetApplicationCommandsInput, + outputSchema: BulkSetApplicationCommandsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/bulkSetGuildApplicationCommands.ts b/packages/discord/src/operations/bulkSetGuildApplicationCommands.ts new file mode 100644 index 000000000..30808d44f --- /dev/null +++ b/packages/discord/src/operations/bulkSetGuildApplicationCommands.ts @@ -0,0 +1,56 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const BulkSetGuildApplicationCommandsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + guild_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "PUT", + path: "/applications/{application_id}/guilds/{guild_id}/commands", + }), + ); +export type BulkSetGuildApplicationCommandsInput = + typeof BulkSetGuildApplicationCommandsInput.Type; + +// Output Schema +export const BulkSetGuildApplicationCommandsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + id: Schema.String, + application_id: Schema.String, + version: Schema.String, + default_member_permissions: Schema.NullOr(Schema.String), + type: Schema.Unknown, + name: Schema.String, + name_localized: Schema.optional(Schema.String), + name_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + description: Schema.String, + description_localized: Schema.optional(Schema.String), + description_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + guild_id: Schema.optional(Schema.String), + dm_permission: Schema.optional(Schema.Boolean), + contexts: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + integration_types: Schema.optional(Schema.Array(Schema.Unknown)), + options: Schema.optional(Schema.Array(Schema.Unknown)), + nsfw: Schema.optional(Schema.Boolean), + }), + ); +export type BulkSetGuildApplicationCommandsOutput = + typeof BulkSetGuildApplicationCommandsOutput.Type; + +// The operation +export const bulkSetGuildApplicationCommands = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: BulkSetGuildApplicationCommandsInput, + outputSchema: BulkSetGuildApplicationCommandsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/bulkUpdateGuildChannels.ts b/packages/discord/src/operations/bulkUpdateGuildChannels.ts new file mode 100644 index 000000000..78bcf25d7 --- /dev/null +++ b/packages/discord/src/operations/bulkUpdateGuildChannels.ts @@ -0,0 +1,27 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const BulkUpdateGuildChannelsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + }).pipe(T.Http({ method: "PATCH", path: "/guilds/{guild_id}/channels" })); +export type BulkUpdateGuildChannelsInput = + typeof BulkUpdateGuildChannelsInput.Type; + +// Output Schema +export const BulkUpdateGuildChannelsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type BulkUpdateGuildChannelsOutput = + typeof BulkUpdateGuildChannelsOutput.Type; + +// The operation +export const bulkUpdateGuildChannels = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: BulkUpdateGuildChannelsInput, + outputSchema: BulkUpdateGuildChannelsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/bulkUpdateGuildRoles.ts b/packages/discord/src/operations/bulkUpdateGuildRoles.ts new file mode 100644 index 000000000..3228e2dc3 --- /dev/null +++ b/packages/discord/src/operations/bulkUpdateGuildRoles.ts @@ -0,0 +1,55 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const BulkUpdateGuildRolesInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + }).pipe(T.Http({ method: "PATCH", path: "/guilds/{guild_id}/roles" })); +export type BulkUpdateGuildRolesInput = typeof BulkUpdateGuildRolesInput.Type; + +// Output Schema +export const BulkUpdateGuildRolesOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ); +export type BulkUpdateGuildRolesOutput = typeof BulkUpdateGuildRolesOutput.Type; + +// The operation +export const bulkUpdateGuildRoles = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: BulkUpdateGuildRolesInput, + outputSchema: BulkUpdateGuildRolesOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/bulkUpdateLobbyMembers.ts b/packages/discord/src/operations/bulkUpdateLobbyMembers.ts new file mode 100644 index 000000000..7b1a47e8b --- /dev/null +++ b/packages/discord/src/operations/bulkUpdateLobbyMembers.ts @@ -0,0 +1,33 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const BulkUpdateLobbyMembersInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + lobby_id: Schema.String.pipe(T.PathParam()), + }).pipe(T.Http({ method: "POST", path: "/lobbies/{lobby_id}/members/bulk" })); +export type BulkUpdateLobbyMembersInput = + typeof BulkUpdateLobbyMembersInput.Type; + +// Output Schema +export const BulkUpdateLobbyMembersOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + id: Schema.String, + metadata: Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + flags: Schema.Number, + }), + ); +export type BulkUpdateLobbyMembersOutput = + typeof BulkUpdateLobbyMembersOutput.Type; + +// The operation +export const bulkUpdateLobbyMembers = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: BulkUpdateLobbyMembersInput, + outputSchema: BulkUpdateLobbyMembersOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/consumeEntitlement.ts b/packages/discord/src/operations/consumeEntitlement.ts new file mode 100644 index 000000000..dfdafb9ec --- /dev/null +++ b/packages/discord/src/operations/consumeEntitlement.ts @@ -0,0 +1,28 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ConsumeEntitlementInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + entitlement_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "POST", + path: "/applications/{application_id}/entitlements/{entitlement_id}/consume", + }), + ); +export type ConsumeEntitlementInput = typeof ConsumeEntitlementInput.Type; + +// Output Schema +export const ConsumeEntitlementOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type ConsumeEntitlementOutput = typeof ConsumeEntitlementOutput.Type; + +// The operation +export const consumeEntitlement = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ConsumeEntitlementInput, + outputSchema: ConsumeEntitlementOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/createApplicationCommand.ts b/packages/discord/src/operations/createApplicationCommand.ts new file mode 100644 index 000000000..ffe6ad706 --- /dev/null +++ b/packages/discord/src/operations/createApplicationCommand.ts @@ -0,0 +1,68 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateApplicationCommandInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + name: Schema.String, + name_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + description: Schema.optional(Schema.NullOr(Schema.String)), + description_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + options: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + default_member_permissions: Schema.optional(Schema.NullOr(Schema.Number)), + dm_permission: Schema.optional(Schema.NullOr(Schema.Boolean)), + contexts: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + integration_types: Schema.optional( + Schema.NullOr(Schema.Array(Schema.Unknown)), + ), + handler: Schema.optional(Schema.Unknown), + type: Schema.optional(Schema.Unknown), + }).pipe( + T.Http({ method: "POST", path: "/applications/{application_id}/commands" }), + ); +export type CreateApplicationCommandInput = + typeof CreateApplicationCommandInput.Type; + +// Output Schema +export const CreateApplicationCommandOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + application_id: Schema.String, + version: Schema.String, + default_member_permissions: Schema.NullOr(Schema.String), + type: Schema.Unknown, + name: Schema.String, + name_localized: Schema.optional(Schema.String), + name_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + description: Schema.String, + description_localized: Schema.optional(Schema.String), + description_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + guild_id: Schema.optional(Schema.String), + dm_permission: Schema.optional(Schema.Boolean), + contexts: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + integration_types: Schema.optional(Schema.Array(Schema.Unknown)), + options: Schema.optional(Schema.Array(Schema.Unknown)), + nsfw: Schema.optional(Schema.Boolean), + }); +export type CreateApplicationCommandOutput = + typeof CreateApplicationCommandOutput.Type; + +// The operation +export const createApplicationCommand = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: CreateApplicationCommandInput, + outputSchema: CreateApplicationCommandOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/createApplicationEmoji.ts b/packages/discord/src/operations/createApplicationEmoji.ts new file mode 100644 index 000000000..c90f99469 --- /dev/null +++ b/packages/discord/src/operations/createApplicationEmoji.ts @@ -0,0 +1,57 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateApplicationEmojiInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + name: Schema.String, + image: Schema.String, + }).pipe( + T.Http({ method: "POST", path: "/applications/{application_id}/emojis" }), + ); +export type CreateApplicationEmojiInput = + typeof CreateApplicationEmojiInput.Type; + +// Output Schema +export const CreateApplicationEmojiOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + roles: Schema.Array(Schema.String), + require_colons: Schema.Boolean, + managed: Schema.Boolean, + animated: Schema.Boolean, + available: Schema.Boolean, + }); +export type CreateApplicationEmojiOutput = + typeof CreateApplicationEmojiOutput.Type; + +// The operation +export const createApplicationEmoji = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: CreateApplicationEmojiInput, + outputSchema: CreateApplicationEmojiOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/createAutoModerationRule.ts b/packages/discord/src/operations/createAutoModerationRule.ts new file mode 100644 index 000000000..cdecb0ba7 --- /dev/null +++ b/packages/discord/src/operations/createAutoModerationRule.ts @@ -0,0 +1,32 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateAutoModerationRuleInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "POST", + path: "/guilds/{guild_id}/auto-moderation/rules", + }), + ); +export type CreateAutoModerationRuleInput = + typeof CreateAutoModerationRuleInput.Type; + +// Output Schema +export const CreateAutoModerationRuleOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Unknown; +export type CreateAutoModerationRuleOutput = + typeof CreateAutoModerationRuleOutput.Type; + +// The operation +export const createAutoModerationRule = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: CreateAutoModerationRuleInput, + outputSchema: CreateAutoModerationRuleOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/createChannelInvite.ts b/packages/discord/src/operations/createChannelInvite.ts new file mode 100644 index 000000000..da1e52ed5 --- /dev/null +++ b/packages/discord/src/operations/createChannelInvite.ts @@ -0,0 +1,23 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateChannelInviteInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + }).pipe(T.Http({ method: "POST", path: "/channels/{channel_id}/invites" })); +export type CreateChannelInviteInput = typeof CreateChannelInviteInput.Type; + +// Output Schema +export const CreateChannelInviteOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Unknown; +export type CreateChannelInviteOutput = typeof CreateChannelInviteOutput.Type; + +// The operation +export const createChannelInvite = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateChannelInviteInput, + outputSchema: CreateChannelInviteOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/createDm.ts b/packages/discord/src/operations/createDm.ts new file mode 100644 index 000000000..224c1199a --- /dev/null +++ b/packages/discord/src/operations/createDm.ts @@ -0,0 +1,25 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateDmInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + recipient_id: Schema.optional(Schema.Unknown), + access_tokens: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + nicks: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.NullOr(Schema.String))), + ), +}).pipe(T.Http({ method: "POST", path: "/users/@me/channels" })); +export type CreateDmInput = typeof CreateDmInput.Type; + +// Output Schema +export const CreateDmOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Unknown; +export type CreateDmOutput = typeof CreateDmOutput.Type; + +// The operation +export const createDm = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateDmInput, + outputSchema: CreateDmOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/createEntitlement.ts b/packages/discord/src/operations/createEntitlement.ts new file mode 100644 index 000000000..aeab2d2a5 --- /dev/null +++ b/packages/discord/src/operations/createEntitlement.ts @@ -0,0 +1,47 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateEntitlementInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + application_id: Schema.String.pipe(T.PathParam()), + sku_id: Schema.String, + owner_id: Schema.String, + owner_type: Schema.Unknown, + }, +).pipe( + T.Http({ + method: "POST", + path: "/applications/{application_id}/entitlements", + }), +); +export type CreateEntitlementInput = typeof CreateEntitlementInput.Type; + +// Output Schema +export const CreateEntitlementOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + sku_id: Schema.String, + application_id: Schema.String, + user_id: Schema.String, + guild_id: Schema.optional(Schema.Unknown), + deleted: Schema.Boolean, + starts_at: Schema.NullOr(Schema.String), + ends_at: Schema.NullOr(Schema.String), + type: Schema.Unknown, + fulfilled_at: Schema.optional(Schema.NullOr(Schema.String)), + fulfillment_status: Schema.optional(Schema.Unknown), + consumed: Schema.optional(Schema.Boolean), + gifter_user_id: Schema.optional(Schema.Unknown), + parent_id: Schema.optional(Schema.Unknown), + }); +export type CreateEntitlementOutput = typeof CreateEntitlementOutput.Type; + +// The operation +export const createEntitlement = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateEntitlementInput, + outputSchema: CreateEntitlementOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/createGuildApplicationCommand.ts b/packages/discord/src/operations/createGuildApplicationCommand.ts new file mode 100644 index 000000000..0165cdeb7 --- /dev/null +++ b/packages/discord/src/operations/createGuildApplicationCommand.ts @@ -0,0 +1,71 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateGuildApplicationCommandInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + guild_id: Schema.String.pipe(T.PathParam()), + name: Schema.String, + name_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + description: Schema.optional(Schema.NullOr(Schema.String)), + description_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + options: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + default_member_permissions: Schema.optional(Schema.NullOr(Schema.Number)), + dm_permission: Schema.optional(Schema.NullOr(Schema.Boolean)), + contexts: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + integration_types: Schema.optional( + Schema.NullOr(Schema.Array(Schema.Unknown)), + ), + handler: Schema.optional(Schema.Unknown), + type: Schema.optional(Schema.Unknown), + }).pipe( + T.Http({ + method: "POST", + path: "/applications/{application_id}/guilds/{guild_id}/commands", + }), + ); +export type CreateGuildApplicationCommandInput = + typeof CreateGuildApplicationCommandInput.Type; + +// Output Schema +export const CreateGuildApplicationCommandOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + application_id: Schema.String, + version: Schema.String, + default_member_permissions: Schema.NullOr(Schema.String), + type: Schema.Unknown, + name: Schema.String, + name_localized: Schema.optional(Schema.String), + name_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + description: Schema.String, + description_localized: Schema.optional(Schema.String), + description_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + guild_id: Schema.optional(Schema.String), + dm_permission: Schema.optional(Schema.Boolean), + contexts: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + integration_types: Schema.optional(Schema.Array(Schema.Unknown)), + options: Schema.optional(Schema.Array(Schema.Unknown)), + nsfw: Schema.optional(Schema.Boolean), + }); +export type CreateGuildApplicationCommandOutput = + typeof CreateGuildApplicationCommandOutput.Type; + +// The operation +export const createGuildApplicationCommand = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateGuildApplicationCommandInput, + outputSchema: CreateGuildApplicationCommandOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/createGuildChannel.ts b/packages/discord/src/operations/createGuildChannel.ts new file mode 100644 index 000000000..140711722 --- /dev/null +++ b/packages/discord/src/operations/createGuildChannel.ts @@ -0,0 +1,104 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateGuildChannelInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + type: Schema.optional(Schema.Unknown), + name: Schema.String, + position: Schema.optional(Schema.NullOr(Schema.Number)), + topic: Schema.optional(Schema.NullOr(Schema.String)), + bitrate: Schema.optional(Schema.NullOr(Schema.Number)), + user_limit: Schema.optional(Schema.NullOr(Schema.Number)), + nsfw: Schema.optional(Schema.NullOr(Schema.Boolean)), + rate_limit_per_user: Schema.optional(Schema.NullOr(Schema.Number)), + parent_id: Schema.optional(Schema.Unknown), + permission_overwrites: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.optional(Schema.Unknown), + allow: Schema.optional(Schema.NullOr(Schema.Number)), + deny: Schema.optional(Schema.NullOr(Schema.Number)), + }), + ), + ), + ), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + default_auto_archive_duration: Schema.optional(Schema.Unknown), + default_reaction_emoji: Schema.optional(Schema.Unknown), + default_thread_rate_limit_per_user: Schema.optional( + Schema.NullOr(Schema.Number), + ), + default_sort_order: Schema.optional(Schema.Unknown), + default_forum_layout: Schema.optional(Schema.Unknown), + default_tag_setting: Schema.optional(Schema.Unknown), + available_tags: Schema.optional( + Schema.NullOr(Schema.Array(Schema.Unknown)), + ), + }).pipe(T.Http({ method: "POST", path: "/guilds/{guild_id}/channels" })); +export type CreateGuildChannelInput = typeof CreateGuildChannelInput.Type; + +// Output Schema +export const CreateGuildChannelOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + topic: Schema.optional(Schema.NullOr(Schema.String)), + default_auto_archive_duration: Schema.optional(Schema.Unknown), + default_thread_rate_limit_per_user: Schema.optional(Schema.Number), + position: Schema.Number, + permission_overwrites: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + allow: Schema.String, + deny: Schema.String, + }), + ), + ), + nsfw: Schema.optional(Schema.Boolean), + available_tags: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + moderated: Schema.Boolean, + emoji_id: Schema.Unknown, + emoji_name: Schema.NullOr(Schema.String), + }), + ), + ), + default_reaction_emoji: Schema.optional(Schema.Unknown), + default_sort_order: Schema.optional(Schema.Unknown), + default_forum_layout: Schema.optional(Schema.Unknown), + default_tag_setting: Schema.optional(Schema.Unknown), + hd_streaming_until: Schema.optional(Schema.String), + hd_streaming_buyer_id: Schema.optional(Schema.String), + }); +export type CreateGuildChannelOutput = typeof CreateGuildChannelOutput.Type; + +// The operation +export const createGuildChannel = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateGuildChannelInput, + outputSchema: CreateGuildChannelOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/createGuildEmoji.ts b/packages/discord/src/operations/createGuildEmoji.ts new file mode 100644 index 000000000..63189c281 --- /dev/null +++ b/packages/discord/src/operations/createGuildEmoji.ts @@ -0,0 +1,52 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateGuildEmojiInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + name: Schema.String, + image: Schema.String, + roles: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), +}).pipe(T.Http({ method: "POST", path: "/guilds/{guild_id}/emojis" })); +export type CreateGuildEmojiInput = typeof CreateGuildEmojiInput.Type; + +// Output Schema +export const CreateGuildEmojiOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + id: Schema.String, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + roles: Schema.Array(Schema.String), + require_colons: Schema.Boolean, + managed: Schema.Boolean, + animated: Schema.Boolean, + available: Schema.Boolean, + }, +); +export type CreateGuildEmojiOutput = typeof CreateGuildEmojiOutput.Type; + +// The operation +export const createGuildEmoji = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateGuildEmojiInput, + outputSchema: CreateGuildEmojiOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/createGuildRole.ts b/packages/discord/src/operations/createGuildRole.ts new file mode 100644 index 000000000..7f43d4101 --- /dev/null +++ b/packages/discord/src/operations/createGuildRole.ts @@ -0,0 +1,57 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateGuildRoleInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + name: Schema.optional(Schema.NullOr(Schema.String)), + permissions: Schema.optional(Schema.NullOr(Schema.Number)), + color: Schema.optional(Schema.NullOr(Schema.Number)), + colors: Schema.optional(Schema.Unknown), + hoist: Schema.optional(Schema.NullOr(Schema.Boolean)), + mentionable: Schema.optional(Schema.NullOr(Schema.Boolean)), + icon: Schema.optional(Schema.NullOr(Schema.String)), + unicode_emoji: Schema.optional(Schema.NullOr(Schema.String)), +}).pipe(T.Http({ method: "POST", path: "/guilds/{guild_id}/roles" })); +export type CreateGuildRoleInput = typeof CreateGuildRoleInput.Type; + +// Output Schema +export const CreateGuildRoleOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, +}); +export type CreateGuildRoleOutput = typeof CreateGuildRoleOutput.Type; + +// The operation +export const createGuildRole = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateGuildRoleInput, + outputSchema: CreateGuildRoleOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/createGuildScheduledEvent.ts b/packages/discord/src/operations/createGuildScheduledEvent.ts new file mode 100644 index 000000000..536cf7f9b --- /dev/null +++ b/packages/discord/src/operations/createGuildScheduledEvent.ts @@ -0,0 +1,29 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateGuildScheduledEventInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ method: "POST", path: "/guilds/{guild_id}/scheduled-events" }), + ); +export type CreateGuildScheduledEventInput = + typeof CreateGuildScheduledEventInput.Type; + +// Output Schema +export const CreateGuildScheduledEventOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Unknown; +export type CreateGuildScheduledEventOutput = + typeof CreateGuildScheduledEventOutput.Type; + +// The operation +export const createGuildScheduledEvent = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: CreateGuildScheduledEventInput, + outputSchema: CreateGuildScheduledEventOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/createGuildSoundboardSound.ts b/packages/discord/src/operations/createGuildSoundboardSound.ts new file mode 100644 index 000000000..605473e52 --- /dev/null +++ b/packages/discord/src/operations/createGuildSoundboardSound.ts @@ -0,0 +1,60 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateGuildSoundboardSoundInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + name: Schema.String, + volume: Schema.optional(Schema.NullOr(Schema.Number)), + emoji_id: Schema.optional(Schema.Unknown), + emoji_name: Schema.optional(Schema.NullOr(Schema.String)), + sound: Schema.String, + }).pipe( + T.Http({ method: "POST", path: "/guilds/{guild_id}/soundboard-sounds" }), + ); +export type CreateGuildSoundboardSoundInput = + typeof CreateGuildSoundboardSoundInput.Type; + +// Output Schema +export const CreateGuildSoundboardSoundOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + name: Schema.String, + sound_id: Schema.String, + volume: Schema.Number, + emoji_id: Schema.Unknown, + emoji_name: Schema.NullOr(Schema.String), + guild_id: Schema.optional(Schema.String), + available: Schema.Boolean, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }); +export type CreateGuildSoundboardSoundOutput = + typeof CreateGuildSoundboardSoundOutput.Type; + +// The operation +export const createGuildSoundboardSound = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: CreateGuildSoundboardSoundInput, + outputSchema: CreateGuildSoundboardSoundOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/createGuildSticker.ts b/packages/discord/src/operations/createGuildSticker.ts new file mode 100644 index 000000000..300d5f2f3 --- /dev/null +++ b/packages/discord/src/operations/createGuildSticker.ts @@ -0,0 +1,60 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateGuildStickerInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + name: Schema.String, + tags: Schema.String, + description: Schema.optional(Schema.NullOr(Schema.String)), + file: Schema.String, + }).pipe( + T.Http({ + method: "POST", + path: "/guilds/{guild_id}/stickers", + contentType: "multipart", + }), + ); +export type CreateGuildStickerInput = typeof CreateGuildStickerInput.Type; + +// Output Schema +export const CreateGuildStickerOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + name: Schema.String, + tags: Schema.String, + type: Schema.Unknown, + format_type: Schema.Unknown, + description: Schema.NullOr(Schema.String), + available: Schema.Boolean, + guild_id: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }); +export type CreateGuildStickerOutput = typeof CreateGuildStickerOutput.Type; + +// The operation +export const createGuildSticker = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateGuildStickerInput, + outputSchema: CreateGuildStickerOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/createGuildTemplate.ts b/packages/discord/src/operations/createGuildTemplate.ts new file mode 100644 index 000000000..2d683563b --- /dev/null +++ b/packages/discord/src/operations/createGuildTemplate.ts @@ -0,0 +1,104 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateGuildTemplateInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + name: Schema.String, + description: Schema.optional(Schema.NullOr(Schema.String)), + }).pipe(T.Http({ method: "POST", path: "/guilds/{guild_id}/templates" })); +export type CreateGuildTemplateInput = typeof CreateGuildTemplateInput.Type; + +// Output Schema +export const CreateGuildTemplateOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + code: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + usage_count: Schema.Number, + creator_id: Schema.String, + creator: Schema.Unknown, + created_at: Schema.String, + updated_at: Schema.String, + source_guild_id: Schema.String, + serialized_source_guild: Schema.Struct({ + name: Schema.String, + description: Schema.NullOr(Schema.String), + region: Schema.NullOr(Schema.String), + verification_level: Schema.Unknown, + default_message_notifications: Schema.Unknown, + explicit_content_filter: Schema.Unknown, + preferred_locale: Schema.Unknown, + afk_channel_id: Schema.Unknown, + afk_timeout: Schema.Unknown, + system_channel_id: Schema.Unknown, + system_channel_flags: Schema.Number, + roles: Schema.Array( + Schema.Struct({ + id: Schema.Number, + name: Schema.String, + permissions: Schema.String, + color: Schema.Number, + colors: Schema.Unknown, + hoist: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + }), + ), + channels: Schema.Array( + Schema.Struct({ + id: Schema.NullOr(Schema.Number), + type: Schema.Unknown, + name: Schema.NullOr(Schema.String), + position: Schema.NullOr(Schema.Number), + topic: Schema.NullOr(Schema.String), + bitrate: Schema.Number, + user_limit: Schema.Number, + nsfw: Schema.Boolean, + rate_limit_per_user: Schema.Number, + parent_id: Schema.Unknown, + default_auto_archive_duration: Schema.Unknown, + permission_overwrites: Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + allow: Schema.String, + deny: Schema.String, + }), + ), + available_tags: Schema.NullOr( + Schema.Array( + Schema.Struct({ + id: Schema.NullOr(Schema.Number), + name: Schema.String, + emoji_id: Schema.Unknown, + emoji_name: Schema.NullOr(Schema.String), + moderated: Schema.NullOr(Schema.Boolean), + }), + ), + ), + template: Schema.String, + default_reaction_emoji: Schema.Unknown, + default_thread_rate_limit_per_user: Schema.NullOr(Schema.Number), + default_sort_order: Schema.Unknown, + default_forum_layout: Schema.Unknown, + default_tag_setting: Schema.Unknown, + icon_emoji: Schema.Unknown, + theme_color: Schema.NullOr(Schema.Number), + }), + ), + }), + is_dirty: Schema.NullOr(Schema.Boolean), + }); +export type CreateGuildTemplateOutput = typeof CreateGuildTemplateOutput.Type; + +// The operation +export const createGuildTemplate = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateGuildTemplateInput, + outputSchema: CreateGuildTemplateOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/createInteractionResponse.ts b/packages/discord/src/operations/createInteractionResponse.ts new file mode 100644 index 000000000..2a2380697 --- /dev/null +++ b/packages/discord/src/operations/createInteractionResponse.ts @@ -0,0 +1,45 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateInteractionResponseInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + interaction_id: Schema.String.pipe(T.PathParam()), + interaction_token: Schema.String.pipe(T.PathParam()), + with_response: Schema.optional(Schema.Boolean), + }).pipe( + T.Http({ + method: "POST", + path: "/interactions/{interaction_id}/{interaction_token}/callback", + }), + ); +export type CreateInteractionResponseInput = + typeof CreateInteractionResponseInput.Type; + +// Output Schema +export const CreateInteractionResponseOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + interaction: Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + response_message_id: Schema.optional(Schema.String), + response_message_loading: Schema.optional(Schema.Boolean), + response_message_ephemeral: Schema.optional(Schema.Boolean), + channel_id: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + }), + resource: Schema.optional(Schema.Unknown), + }); +export type CreateInteractionResponseOutput = + typeof CreateInteractionResponseOutput.Type; + +// The operation +export const createInteractionResponse = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: CreateInteractionResponseInput, + outputSchema: CreateInteractionResponseOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/createLinkedLobbyGuildInviteForSelf.ts b/packages/discord/src/operations/createLinkedLobbyGuildInviteForSelf.ts new file mode 100644 index 000000000..ba2deab6a --- /dev/null +++ b/packages/discord/src/operations/createLinkedLobbyGuildInviteForSelf.ts @@ -0,0 +1,30 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateLinkedLobbyGuildInviteForSelfInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + lobby_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ method: "POST", path: "/lobbies/{lobby_id}/members/@me/invites" }), + ); +export type CreateLinkedLobbyGuildInviteForSelfInput = + typeof CreateLinkedLobbyGuildInviteForSelfInput.Type; + +// Output Schema +export const CreateLinkedLobbyGuildInviteForSelfOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + code: Schema.String, + }); +export type CreateLinkedLobbyGuildInviteForSelfOutput = + typeof CreateLinkedLobbyGuildInviteForSelfOutput.Type; + +// The operation +export const createLinkedLobbyGuildInviteForSelf = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateLinkedLobbyGuildInviteForSelfInput, + outputSchema: CreateLinkedLobbyGuildInviteForSelfOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/createLinkedLobbyGuildInviteForUser.ts b/packages/discord/src/operations/createLinkedLobbyGuildInviteForUser.ts new file mode 100644 index 000000000..ae6ba0b03 --- /dev/null +++ b/packages/discord/src/operations/createLinkedLobbyGuildInviteForUser.ts @@ -0,0 +1,34 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateLinkedLobbyGuildInviteForUserInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + lobby_id: Schema.String.pipe(T.PathParam()), + user_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "POST", + path: "/lobbies/{lobby_id}/members/{user_id}/invites", + }), + ); +export type CreateLinkedLobbyGuildInviteForUserInput = + typeof CreateLinkedLobbyGuildInviteForUserInput.Type; + +// Output Schema +export const CreateLinkedLobbyGuildInviteForUserOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + code: Schema.String, + }); +export type CreateLinkedLobbyGuildInviteForUserOutput = + typeof CreateLinkedLobbyGuildInviteForUserOutput.Type; + +// The operation +export const createLinkedLobbyGuildInviteForUser = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateLinkedLobbyGuildInviteForUserInput, + outputSchema: CreateLinkedLobbyGuildInviteForUserOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/createLobby.ts b/packages/discord/src/operations/createLobby.ts new file mode 100644 index 000000000..ee29021a3 --- /dev/null +++ b/packages/discord/src/operations/createLobby.ts @@ -0,0 +1,102 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateLobbyInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + idle_timeout_seconds: Schema.optional(Schema.NullOr(Schema.Number)), + members: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + id: Schema.String, + metadata: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + flags: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + metadata: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + flags: Schema.optional(Schema.Unknown), + override_event_webhooks_url: Schema.optional(Schema.NullOr(Schema.String)), +}).pipe(T.Http({ method: "POST", path: "/lobbies" })); +export type CreateLobbyInput = typeof CreateLobbyInput.Type; + +// Output Schema +export const CreateLobbyOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + application_id: Schema.String, + metadata: Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + members: Schema.Array( + Schema.Struct({ + id: Schema.String, + metadata: Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + flags: Schema.Number, + }), + ), + linked_channel: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + topic: Schema.optional(Schema.NullOr(Schema.String)), + default_auto_archive_duration: Schema.optional(Schema.Unknown), + default_thread_rate_limit_per_user: Schema.optional(Schema.Number), + position: Schema.Number, + permission_overwrites: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + allow: Schema.String, + deny: Schema.String, + }), + ), + ), + nsfw: Schema.optional(Schema.Boolean), + available_tags: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + moderated: Schema.Boolean, + emoji_id: Schema.Unknown, + emoji_name: Schema.NullOr(Schema.String), + }), + ), + ), + default_reaction_emoji: Schema.optional(Schema.Unknown), + default_sort_order: Schema.optional(Schema.Unknown), + default_forum_layout: Schema.optional(Schema.Unknown), + default_tag_setting: Schema.optional(Schema.Unknown), + hd_streaming_until: Schema.optional(Schema.String), + hd_streaming_buyer_id: Schema.optional(Schema.String), + }), + ), + flags: Schema.Number, + override_event_webhooks_url: Schema.optional(Schema.NullOr(Schema.String)), +}); +export type CreateLobbyOutput = typeof CreateLobbyOutput.Type; + +// The operation +export const createLobby = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateLobbyInput, + outputSchema: CreateLobbyOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/createLobbyMessage.ts b/packages/discord/src/operations/createLobbyMessage.ts new file mode 100644 index 000000000..3cadbb12c --- /dev/null +++ b/packages/discord/src/operations/createLobbyMessage.ts @@ -0,0 +1,108 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateLobbyMessageInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + lobby_id: Schema.String.pipe(T.PathParam()), + content: Schema.optional(Schema.NullOr(Schema.String)), + embeds: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + type: Schema.optional(Schema.NullOr(Schema.String)), + url: Schema.optional(Schema.NullOr(Schema.String)), + title: Schema.optional(Schema.NullOr(Schema.String)), + color: Schema.optional(Schema.NullOr(Schema.Number)), + timestamp: Schema.optional(Schema.NullOr(Schema.String)), + description: Schema.optional(Schema.NullOr(Schema.String)), + author: Schema.optional(Schema.Unknown), + image: Schema.optional(Schema.Unknown), + thumbnail: Schema.optional(Schema.Unknown), + footer: Schema.optional(Schema.Unknown), + fields: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.optional(Schema.NullOr(Schema.Boolean)), + }), + ), + ), + ), + provider: Schema.optional(Schema.Unknown), + video: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + allowed_mentions: Schema.optional(Schema.Unknown), + sticker_ids: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + components: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + attachments: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.optional(Schema.NullOr(Schema.String)), + description: Schema.optional(Schema.NullOr(Schema.String)), + duration_secs: Schema.optional(Schema.NullOr(Schema.Number)), + waveform: Schema.optional(Schema.NullOr(Schema.String)), + title: Schema.optional(Schema.NullOr(Schema.String)), + is_remix: Schema.optional(Schema.NullOr(Schema.Boolean)), + }), + ), + ), + ), + poll: Schema.optional(Schema.Unknown), + shared_client_theme: Schema.optional(Schema.Unknown), + message_reference: Schema.optional(Schema.Unknown), + nonce: Schema.optional(Schema.Unknown), + enforce_nonce: Schema.optional(Schema.NullOr(Schema.Boolean)), + tts: Schema.optional(Schema.NullOr(Schema.Boolean)), + }).pipe(T.Http({ method: "POST", path: "/lobbies/{lobby_id}/messages" })); +export type CreateLobbyMessageInput = typeof CreateLobbyMessageInput.Type; + +// Output Schema +export const CreateLobbyMessageOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + content: Schema.String, + lobby_id: Schema.String, + channel_id: Schema.String, + author: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), + moderation_metadata: Schema.optional( + Schema.Record(Schema.String, Schema.String), + ), + flags: Schema.Number, + application_id: Schema.optional(Schema.String), + }); +export type CreateLobbyMessageOutput = typeof CreateLobbyMessageOutput.Type; + +// The operation +export const createLobbyMessage = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateLobbyMessageInput, + outputSchema: CreateLobbyMessageOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/createMessage.ts b/packages/discord/src/operations/createMessage.ts new file mode 100644 index 000000000..661914d8f --- /dev/null +++ b/packages/discord/src/operations/createMessage.ts @@ -0,0 +1,905 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateMessageInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + content: Schema.optional(Schema.NullOr(Schema.String)), + embeds: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + type: Schema.optional(Schema.NullOr(Schema.String)), + url: Schema.optional(Schema.NullOr(Schema.String)), + title: Schema.optional(Schema.NullOr(Schema.String)), + color: Schema.optional(Schema.NullOr(Schema.Number)), + timestamp: Schema.optional(Schema.NullOr(Schema.String)), + description: Schema.optional(Schema.NullOr(Schema.String)), + author: Schema.optional(Schema.Unknown), + image: Schema.optional(Schema.Unknown), + thumbnail: Schema.optional(Schema.Unknown), + footer: Schema.optional(Schema.Unknown), + fields: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.optional(Schema.NullOr(Schema.Boolean)), + }), + ), + ), + ), + provider: Schema.optional(Schema.Unknown), + video: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + allowed_mentions: Schema.optional(Schema.Unknown), + sticker_ids: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + components: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + attachments: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.optional(Schema.NullOr(Schema.String)), + description: Schema.optional(Schema.NullOr(Schema.String)), + duration_secs: Schema.optional(Schema.NullOr(Schema.Number)), + waveform: Schema.optional(Schema.NullOr(Schema.String)), + title: Schema.optional(Schema.NullOr(Schema.String)), + is_remix: Schema.optional(Schema.NullOr(Schema.Boolean)), + }), + ), + ), + ), + poll: Schema.optional(Schema.Unknown), + shared_client_theme: Schema.optional(Schema.Unknown), + message_reference: Schema.optional(Schema.Unknown), + nonce: Schema.optional(Schema.Unknown), + enforce_nonce: Schema.optional(Schema.NullOr(Schema.Boolean)), + tts: Schema.optional(Schema.NullOr(Schema.Boolean)), +}).pipe(T.Http({ method: "POST", path: "/channels/{channel_id}/messages" })); +export type CreateMessageInput = typeof CreateMessageInput.Type; + +// Output Schema +export const CreateMessageOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + id: Schema.String, + channel_id: Schema.String, + author: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + pinned: Schema.Boolean, + mention_everyone: Schema.Boolean, + tts: Schema.Boolean, + call: Schema.optional( + Schema.Struct({ + ended_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + participants: Schema.Array(Schema.String), + }), + ), + activity: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + party_id: Schema.optional(Schema.String), + }), + ), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + application_id: Schema.optional(Schema.String), + interaction: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + name_localized: Schema.optional(Schema.String), + }), + ), + nonce: Schema.optional(Schema.Unknown), + webhook_id: Schema.optional(Schema.String), + message_reference: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + channel_id: Schema.String, + message_id: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + }), + ), + thread: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + mention_channels: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + type: Schema.Unknown, + guild_id: Schema.String, + }), + ), + ), + role_subscription_data: Schema.optional( + Schema.Struct({ + role_subscription_listing_id: Schema.String, + tier_name: Schema.String, + total_months_subscribed: Schema.Number, + is_renewal: Schema.Boolean, + }), + ), + purchase_notification: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + guild_product_purchase: Schema.optional( + Schema.Struct({ + listing_id: Schema.String, + product_name: Schema.String, + }), + ), + }), + ), + position: Schema.optional(Schema.Number), + resolved: Schema.optional( + Schema.Struct({ + users: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + ), + members: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + channels: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + ), + roles: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ), + ), + ), + }), + ), + poll: Schema.optional( + Schema.Struct({ + question: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + answers: Schema.Array( + Schema.Struct({ + answer_id: Schema.Number, + poll_media: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + }), + ), + expiry: Schema.String, + allow_multiselect: Schema.Boolean, + layout_type: Schema.Unknown, + results: Schema.Struct({ + answer_counts: Schema.Array( + Schema.Struct({ + id: Schema.Number, + count: Schema.Number, + me_voted: Schema.Boolean, + }), + ), + is_finalized: Schema.Boolean, + }), + }), + ), + shared_client_theme: Schema.optional( + Schema.Struct({ + colors: Schema.Array(Schema.String), + gradient_angle: Schema.Number, + base_mix: Schema.Number, + base_theme: Schema.Unknown, + }), + ), + interaction_metadata: Schema.optional(Schema.Unknown), + message_snapshots: Schema.optional( + Schema.Array( + Schema.Struct({ + message: Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + }), + }), + ), + ), + reactions: Schema.optional( + Schema.Array( + Schema.Struct({ + emoji: Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + count: Schema.Number, + count_details: Schema.Struct({ + burst: Schema.Number, + normal: Schema.Number, + }), + burst_colors: Schema.Array(Schema.String), + me_burst: Schema.Boolean, + me: Schema.Boolean, + }), + ), + ), + referenced_message: Schema.optional(Schema.Unknown), +}); +export type CreateMessageOutput = typeof CreateMessageOutput.Type; + +// The operation +export const createMessage = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateMessageInput, + outputSchema: CreateMessageOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/createOrJoinLobby.ts b/packages/discord/src/operations/createOrJoinLobby.ts new file mode 100644 index 000000000..4bb637878 --- /dev/null +++ b/packages/discord/src/operations/createOrJoinLobby.ts @@ -0,0 +1,96 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; +import { SensitiveString } from "../sensitive.ts"; + +// Input Schema +export const CreateOrJoinLobbyInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + idle_timeout_seconds: Schema.optional(Schema.NullOr(Schema.Number)), + lobby_metadata: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + member_metadata: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + secret: SensitiveString, + flags: Schema.optional(Schema.Unknown), + }, +).pipe(T.Http({ method: "PUT", path: "/lobbies" })); +export type CreateOrJoinLobbyInput = typeof CreateOrJoinLobbyInput.Type; + +// Output Schema +export const CreateOrJoinLobbyOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + application_id: Schema.String, + metadata: Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + members: Schema.Array( + Schema.Struct({ + id: Schema.String, + metadata: Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + flags: Schema.Number, + }), + ), + linked_channel: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + topic: Schema.optional(Schema.NullOr(Schema.String)), + default_auto_archive_duration: Schema.optional(Schema.Unknown), + default_thread_rate_limit_per_user: Schema.optional(Schema.Number), + position: Schema.Number, + permission_overwrites: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + allow: Schema.String, + deny: Schema.String, + }), + ), + ), + nsfw: Schema.optional(Schema.Boolean), + available_tags: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + moderated: Schema.Boolean, + emoji_id: Schema.Unknown, + emoji_name: Schema.NullOr(Schema.String), + }), + ), + ), + default_reaction_emoji: Schema.optional(Schema.Unknown), + default_sort_order: Schema.optional(Schema.Unknown), + default_forum_layout: Schema.optional(Schema.Unknown), + default_tag_setting: Schema.optional(Schema.Unknown), + hd_streaming_until: Schema.optional(Schema.String), + hd_streaming_buyer_id: Schema.optional(Schema.String), + }), + ), + flags: Schema.Number, + override_event_webhooks_url: Schema.optional(Schema.NullOr(Schema.String)), + }); +export type CreateOrJoinLobbyOutput = typeof CreateOrJoinLobbyOutput.Type; + +// The operation +export const createOrJoinLobby = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateOrJoinLobbyInput, + outputSchema: CreateOrJoinLobbyOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/createPin.ts b/packages/discord/src/operations/createPin.ts new file mode 100644 index 000000000..4b4b8e00b --- /dev/null +++ b/packages/discord/src/operations/createPin.ts @@ -0,0 +1,27 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreatePinInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), +}).pipe( + T.Http({ + method: "PUT", + path: "/channels/{channel_id}/messages/pins/{message_id}", + }), +); +export type CreatePinInput = typeof CreatePinInput.Type; + +// Output Schema +export const CreatePinOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type CreatePinOutput = typeof CreatePinOutput.Type; + +// The operation +export const createPin = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreatePinInput, + outputSchema: CreatePinOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/createStageInstance.ts b/packages/discord/src/operations/createStageInstance.ts new file mode 100644 index 000000000..e817ac49b --- /dev/null +++ b/packages/discord/src/operations/createStageInstance.ts @@ -0,0 +1,35 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateStageInstanceInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + topic: Schema.String, + channel_id: Schema.String, + privacy_level: Schema.optional(Schema.Unknown), + guild_scheduled_event_id: Schema.optional(Schema.Unknown), + send_start_notification: Schema.optional(Schema.NullOr(Schema.Boolean)), + }).pipe(T.Http({ method: "POST", path: "/stage-instances" })); +export type CreateStageInstanceInput = typeof CreateStageInstanceInput.Type; + +// Output Schema +export const CreateStageInstanceOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String, + channel_id: Schema.String, + topic: Schema.String, + privacy_level: Schema.Unknown, + id: Schema.String, + discoverable_disabled: Schema.Boolean, + guild_scheduled_event_id: Schema.Unknown, + }); +export type CreateStageInstanceOutput = typeof CreateStageInstanceOutput.Type; + +// The operation +export const createStageInstance = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateStageInstanceInput, + outputSchema: CreateStageInstanceOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/createThread.ts b/packages/discord/src/operations/createThread.ts new file mode 100644 index 000000000..c0fb88ae6 --- /dev/null +++ b/packages/discord/src/operations/createThread.ts @@ -0,0 +1,90 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateThreadInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "POST", path: "/channels/{channel_id}/threads" })); +export type CreateThreadInput = typeof CreateThreadInput.Type; + +// Output Schema +export const CreateThreadOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), +}); +export type CreateThreadOutput = typeof CreateThreadOutput.Type; + +// The operation +export const createThread = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateThreadInput, + outputSchema: CreateThreadOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/createThreadFromMessage.ts b/packages/discord/src/operations/createThreadFromMessage.ts new file mode 100644 index 000000000..669bbfcb5 --- /dev/null +++ b/packages/discord/src/operations/createThreadFromMessage.ts @@ -0,0 +1,105 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateThreadFromMessageInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), + name: Schema.String, + auto_archive_duration: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.NullOr(Schema.Number)), + }).pipe( + T.Http({ + method: "POST", + path: "/channels/{channel_id}/messages/{message_id}/threads", + }), + ); +export type CreateThreadFromMessageInput = + typeof CreateThreadFromMessageInput.Type; + +// Output Schema +export const CreateThreadFromMessageOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }); +export type CreateThreadFromMessageOutput = + typeof CreateThreadFromMessageOutput.Type; + +// The operation +export const createThreadFromMessage = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: CreateThreadFromMessageInput, + outputSchema: CreateThreadFromMessageOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/createWebhook.ts b/packages/discord/src/operations/createWebhook.ts new file mode 100644 index 000000000..6c16876ef --- /dev/null +++ b/packages/discord/src/operations/createWebhook.ts @@ -0,0 +1,51 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CreateWebhookInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + name: Schema.String, + avatar: Schema.optional(Schema.NullOr(Schema.String)), +}).pipe(T.Http({ method: "POST", path: "/channels/{channel_id}/webhooks" })); +export type CreateWebhookInput = typeof CreateWebhookInput.Type; + +// Output Schema +export const CreateWebhookOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.Unknown, + avatar: Schema.NullOr(Schema.String), + channel_id: Schema.Unknown, + guild_id: Schema.optional(Schema.Unknown), + id: Schema.String, + name: Schema.String, + type: Schema.Unknown, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + token: Schema.optional(Schema.String), + url: Schema.optional(Schema.String), +}); +export type CreateWebhookOutput = typeof CreateWebhookOutput.Type; + +// The operation +export const createWebhook = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateWebhookInput, + outputSchema: CreateWebhookOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/crosspostMessage.ts b/packages/discord/src/operations/crosspostMessage.ts new file mode 100644 index 000000000..cb8b901b0 --- /dev/null +++ b/packages/discord/src/operations/crosspostMessage.ts @@ -0,0 +1,858 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CrosspostMessageInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), +}).pipe( + T.Http({ + method: "POST", + path: "/channels/{channel_id}/messages/{message_id}/crosspost", + }), +); +export type CrosspostMessageInput = typeof CrosspostMessageInput.Type; + +// Output Schema +export const CrosspostMessageOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + id: Schema.String, + channel_id: Schema.String, + author: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + pinned: Schema.Boolean, + mention_everyone: Schema.Boolean, + tts: Schema.Boolean, + call: Schema.optional( + Schema.Struct({ + ended_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + participants: Schema.Array(Schema.String), + }), + ), + activity: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + party_id: Schema.optional(Schema.String), + }), + ), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + application_id: Schema.optional(Schema.String), + interaction: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + name_localized: Schema.optional(Schema.String), + }), + ), + nonce: Schema.optional(Schema.Unknown), + webhook_id: Schema.optional(Schema.String), + message_reference: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + channel_id: Schema.String, + message_id: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + }), + ), + thread: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + mention_channels: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + type: Schema.Unknown, + guild_id: Schema.String, + }), + ), + ), + role_subscription_data: Schema.optional( + Schema.Struct({ + role_subscription_listing_id: Schema.String, + tier_name: Schema.String, + total_months_subscribed: Schema.Number, + is_renewal: Schema.Boolean, + }), + ), + purchase_notification: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + guild_product_purchase: Schema.optional( + Schema.Struct({ + listing_id: Schema.String, + product_name: Schema.String, + }), + ), + }), + ), + position: Schema.optional(Schema.Number), + resolved: Schema.optional( + Schema.Struct({ + users: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + ), + members: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + channels: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + ), + roles: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ), + ), + ), + }), + ), + poll: Schema.optional( + Schema.Struct({ + question: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + answers: Schema.Array( + Schema.Struct({ + answer_id: Schema.Number, + poll_media: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + }), + ), + expiry: Schema.String, + allow_multiselect: Schema.Boolean, + layout_type: Schema.Unknown, + results: Schema.Struct({ + answer_counts: Schema.Array( + Schema.Struct({ + id: Schema.Number, + count: Schema.Number, + me_voted: Schema.Boolean, + }), + ), + is_finalized: Schema.Boolean, + }), + }), + ), + shared_client_theme: Schema.optional( + Schema.Struct({ + colors: Schema.Array(Schema.String), + gradient_angle: Schema.Number, + base_mix: Schema.Number, + base_theme: Schema.Unknown, + }), + ), + interaction_metadata: Schema.optional(Schema.Unknown), + message_snapshots: Schema.optional( + Schema.Array( + Schema.Struct({ + message: Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + }), + }), + ), + ), + reactions: Schema.optional( + Schema.Array( + Schema.Struct({ + emoji: Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + count: Schema.Number, + count_details: Schema.Struct({ + burst: Schema.Number, + normal: Schema.Number, + }), + burst_colors: Schema.Array(Schema.String), + me_burst: Schema.Boolean, + me: Schema.Boolean, + }), + ), + ), + referenced_message: Schema.optional(Schema.Unknown), + }, +); +export type CrosspostMessageOutput = typeof CrosspostMessageOutput.Type; + +// The operation +export const crosspostMessage = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CrosspostMessageInput, + outputSchema: CrosspostMessageOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/deleteAllMessageReactions.ts b/packages/discord/src/operations/deleteAllMessageReactions.ts new file mode 100644 index 000000000..ba7815058 --- /dev/null +++ b/packages/discord/src/operations/deleteAllMessageReactions.ts @@ -0,0 +1,33 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteAllMessageReactionsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "DELETE", + path: "/channels/{channel_id}/messages/{message_id}/reactions", + }), + ); +export type DeleteAllMessageReactionsInput = + typeof DeleteAllMessageReactionsInput.Type; + +// Output Schema +export const DeleteAllMessageReactionsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteAllMessageReactionsOutput = + typeof DeleteAllMessageReactionsOutput.Type; + +// The operation +export const deleteAllMessageReactions = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: DeleteAllMessageReactionsInput, + outputSchema: DeleteAllMessageReactionsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/deleteAllMessageReactionsByEmoji.ts b/packages/discord/src/operations/deleteAllMessageReactionsByEmoji.ts new file mode 100644 index 000000000..2771e1290 --- /dev/null +++ b/packages/discord/src/operations/deleteAllMessageReactionsByEmoji.ts @@ -0,0 +1,33 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteAllMessageReactionsByEmojiInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), + emoji_name: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "DELETE", + path: "/channels/{channel_id}/messages/{message_id}/reactions/{emoji_name}", + }), + ); +export type DeleteAllMessageReactionsByEmojiInput = + typeof DeleteAllMessageReactionsByEmojiInput.Type; + +// Output Schema +export const DeleteAllMessageReactionsByEmojiOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteAllMessageReactionsByEmojiOutput = + typeof DeleteAllMessageReactionsByEmojiOutput.Type; + +// The operation +export const deleteAllMessageReactionsByEmoji = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteAllMessageReactionsByEmojiInput, + outputSchema: DeleteAllMessageReactionsByEmojiOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/deleteApplicationCommand.ts b/packages/discord/src/operations/deleteApplicationCommand.ts new file mode 100644 index 000000000..91570bf70 --- /dev/null +++ b/packages/discord/src/operations/deleteApplicationCommand.ts @@ -0,0 +1,33 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteApplicationCommandInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + command_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "DELETE", + path: "/applications/{application_id}/commands/{command_id}", + }), + ); +export type DeleteApplicationCommandInput = + typeof DeleteApplicationCommandInput.Type; + +// Output Schema +export const DeleteApplicationCommandOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteApplicationCommandOutput = + typeof DeleteApplicationCommandOutput.Type; + +// The operation +export const deleteApplicationCommand = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: DeleteApplicationCommandInput, + outputSchema: DeleteApplicationCommandOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/deleteApplicationEmoji.ts b/packages/discord/src/operations/deleteApplicationEmoji.ts new file mode 100644 index 000000000..8a2f10bbc --- /dev/null +++ b/packages/discord/src/operations/deleteApplicationEmoji.ts @@ -0,0 +1,33 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteApplicationEmojiInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + emoji_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "DELETE", + path: "/applications/{application_id}/emojis/{emoji_id}", + }), + ); +export type DeleteApplicationEmojiInput = + typeof DeleteApplicationEmojiInput.Type; + +// Output Schema +export const DeleteApplicationEmojiOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteApplicationEmojiOutput = + typeof DeleteApplicationEmojiOutput.Type; + +// The operation +export const deleteApplicationEmoji = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: DeleteApplicationEmojiInput, + outputSchema: DeleteApplicationEmojiOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/deleteApplicationUserRoleConnection.ts b/packages/discord/src/operations/deleteApplicationUserRoleConnection.ts new file mode 100644 index 000000000..948f20724 --- /dev/null +++ b/packages/discord/src/operations/deleteApplicationUserRoleConnection.ts @@ -0,0 +1,31 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteApplicationUserRoleConnectionInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "DELETE", + path: "/users/@me/applications/{application_id}/role-connection", + }), + ); +export type DeleteApplicationUserRoleConnectionInput = + typeof DeleteApplicationUserRoleConnectionInput.Type; + +// Output Schema +export const DeleteApplicationUserRoleConnectionOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteApplicationUserRoleConnectionOutput = + typeof DeleteApplicationUserRoleConnectionOutput.Type; + +// The operation +export const deleteApplicationUserRoleConnection = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteApplicationUserRoleConnectionInput, + outputSchema: DeleteApplicationUserRoleConnectionOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/deleteAutoModerationRule.ts b/packages/discord/src/operations/deleteAutoModerationRule.ts new file mode 100644 index 000000000..be06a06b3 --- /dev/null +++ b/packages/discord/src/operations/deleteAutoModerationRule.ts @@ -0,0 +1,33 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteAutoModerationRuleInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + rule_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "DELETE", + path: "/guilds/{guild_id}/auto-moderation/rules/{rule_id}", + }), + ); +export type DeleteAutoModerationRuleInput = + typeof DeleteAutoModerationRuleInput.Type; + +// Output Schema +export const DeleteAutoModerationRuleOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteAutoModerationRuleOutput = + typeof DeleteAutoModerationRuleOutput.Type; + +// The operation +export const deleteAutoModerationRule = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: DeleteAutoModerationRuleInput, + outputSchema: DeleteAutoModerationRuleOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/deleteChannel.ts b/packages/discord/src/operations/deleteChannel.ts new file mode 100644 index 000000000..206e3d2fd --- /dev/null +++ b/packages/discord/src/operations/deleteChannel.ts @@ -0,0 +1,21 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteChannelInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "DELETE", path: "/channels/{channel_id}" })); +export type DeleteChannelInput = typeof DeleteChannelInput.Type; + +// Output Schema +export const DeleteChannelOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Unknown; +export type DeleteChannelOutput = typeof DeleteChannelOutput.Type; + +// The operation +export const deleteChannel = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteChannelInput, + outputSchema: DeleteChannelOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/deleteChannelPermissionOverwrite.ts b/packages/discord/src/operations/deleteChannelPermissionOverwrite.ts new file mode 100644 index 000000000..6c7060714 --- /dev/null +++ b/packages/discord/src/operations/deleteChannelPermissionOverwrite.ts @@ -0,0 +1,32 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteChannelPermissionOverwriteInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + overwrite_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "DELETE", + path: "/channels/{channel_id}/permissions/{overwrite_id}", + }), + ); +export type DeleteChannelPermissionOverwriteInput = + typeof DeleteChannelPermissionOverwriteInput.Type; + +// Output Schema +export const DeleteChannelPermissionOverwriteOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteChannelPermissionOverwriteOutput = + typeof DeleteChannelPermissionOverwriteOutput.Type; + +// The operation +export const deleteChannelPermissionOverwrite = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteChannelPermissionOverwriteInput, + outputSchema: DeleteChannelPermissionOverwriteOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/deleteEntitlement.ts b/packages/discord/src/operations/deleteEntitlement.ts new file mode 100644 index 000000000..7a0c32edb --- /dev/null +++ b/packages/discord/src/operations/deleteEntitlement.ts @@ -0,0 +1,29 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteEntitlementInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + application_id: Schema.String.pipe(T.PathParam()), + entitlement_id: Schema.String.pipe(T.PathParam()), + }, +).pipe( + T.Http({ + method: "DELETE", + path: "/applications/{application_id}/entitlements/{entitlement_id}", + }), +); +export type DeleteEntitlementInput = typeof DeleteEntitlementInput.Type; + +// Output Schema +export const DeleteEntitlementOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteEntitlementOutput = typeof DeleteEntitlementOutput.Type; + +// The operation +export const deleteEntitlement = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteEntitlementInput, + outputSchema: DeleteEntitlementOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/deleteGroupDmUser.ts b/packages/discord/src/operations/deleteGroupDmUser.ts new file mode 100644 index 000000000..4a1c7e699 --- /dev/null +++ b/packages/discord/src/operations/deleteGroupDmUser.ts @@ -0,0 +1,29 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteGroupDmUserInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + channel_id: Schema.String.pipe(T.PathParam()), + user_id: Schema.String.pipe(T.PathParam()), + }, +).pipe( + T.Http({ + method: "DELETE", + path: "/channels/{channel_id}/recipients/{user_id}", + }), +); +export type DeleteGroupDmUserInput = typeof DeleteGroupDmUserInput.Type; + +// Output Schema +export const DeleteGroupDmUserOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteGroupDmUserOutput = typeof DeleteGroupDmUserOutput.Type; + +// The operation +export const deleteGroupDmUser = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteGroupDmUserInput, + outputSchema: DeleteGroupDmUserOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/deleteGuildApplicationCommand.ts b/packages/discord/src/operations/deleteGuildApplicationCommand.ts new file mode 100644 index 000000000..b145e8317 --- /dev/null +++ b/packages/discord/src/operations/deleteGuildApplicationCommand.ts @@ -0,0 +1,33 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteGuildApplicationCommandInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + guild_id: Schema.String.pipe(T.PathParam()), + command_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "DELETE", + path: "/applications/{application_id}/guilds/{guild_id}/commands/{command_id}", + }), + ); +export type DeleteGuildApplicationCommandInput = + typeof DeleteGuildApplicationCommandInput.Type; + +// Output Schema +export const DeleteGuildApplicationCommandOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteGuildApplicationCommandOutput = + typeof DeleteGuildApplicationCommandOutput.Type; + +// The operation +export const deleteGuildApplicationCommand = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteGuildApplicationCommandInput, + outputSchema: DeleteGuildApplicationCommandOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/deleteGuildEmoji.ts b/packages/discord/src/operations/deleteGuildEmoji.ts new file mode 100644 index 000000000..08a238d5c --- /dev/null +++ b/packages/discord/src/operations/deleteGuildEmoji.ts @@ -0,0 +1,24 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteGuildEmojiInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + emoji_id: Schema.String.pipe(T.PathParam()), +}).pipe( + T.Http({ method: "DELETE", path: "/guilds/{guild_id}/emojis/{emoji_id}" }), +); +export type DeleteGuildEmojiInput = typeof DeleteGuildEmojiInput.Type; + +// Output Schema +export const DeleteGuildEmojiOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteGuildEmojiOutput = typeof DeleteGuildEmojiOutput.Type; + +// The operation +export const deleteGuildEmoji = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteGuildEmojiInput, + outputSchema: DeleteGuildEmojiOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/deleteGuildIntegration.ts b/packages/discord/src/operations/deleteGuildIntegration.ts new file mode 100644 index 000000000..5d3837772 --- /dev/null +++ b/packages/discord/src/operations/deleteGuildIntegration.ts @@ -0,0 +1,33 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteGuildIntegrationInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + integration_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "DELETE", + path: "/guilds/{guild_id}/integrations/{integration_id}", + }), + ); +export type DeleteGuildIntegrationInput = + typeof DeleteGuildIntegrationInput.Type; + +// Output Schema +export const DeleteGuildIntegrationOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteGuildIntegrationOutput = + typeof DeleteGuildIntegrationOutput.Type; + +// The operation +export const deleteGuildIntegration = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: DeleteGuildIntegrationInput, + outputSchema: DeleteGuildIntegrationOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/deleteGuildMember.ts b/packages/discord/src/operations/deleteGuildMember.ts new file mode 100644 index 000000000..265aa1561 --- /dev/null +++ b/packages/discord/src/operations/deleteGuildMember.ts @@ -0,0 +1,26 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteGuildMemberInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + guild_id: Schema.String.pipe(T.PathParam()), + user_id: Schema.String.pipe(T.PathParam()), + }, +).pipe( + T.Http({ method: "DELETE", path: "/guilds/{guild_id}/members/{user_id}" }), +); +export type DeleteGuildMemberInput = typeof DeleteGuildMemberInput.Type; + +// Output Schema +export const DeleteGuildMemberOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteGuildMemberOutput = typeof DeleteGuildMemberOutput.Type; + +// The operation +export const deleteGuildMember = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteGuildMemberInput, + outputSchema: DeleteGuildMemberOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/deleteGuildMemberRole.ts b/packages/discord/src/operations/deleteGuildMemberRole.ts new file mode 100644 index 000000000..3748e70c1 --- /dev/null +++ b/packages/discord/src/operations/deleteGuildMemberRole.ts @@ -0,0 +1,33 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteGuildMemberRoleInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + user_id: Schema.String.pipe(T.PathParam()), + role_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "DELETE", + path: "/guilds/{guild_id}/members/{user_id}/roles/{role_id}", + }), + ); +export type DeleteGuildMemberRoleInput = typeof DeleteGuildMemberRoleInput.Type; + +// Output Schema +export const DeleteGuildMemberRoleOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteGuildMemberRoleOutput = + typeof DeleteGuildMemberRoleOutput.Type; + +// The operation +export const deleteGuildMemberRole = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: DeleteGuildMemberRoleInput, + outputSchema: DeleteGuildMemberRoleOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/deleteGuildRole.ts b/packages/discord/src/operations/deleteGuildRole.ts new file mode 100644 index 000000000..b73e8818e --- /dev/null +++ b/packages/discord/src/operations/deleteGuildRole.ts @@ -0,0 +1,24 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteGuildRoleInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + role_id: Schema.String.pipe(T.PathParam()), +}).pipe( + T.Http({ method: "DELETE", path: "/guilds/{guild_id}/roles/{role_id}" }), +); +export type DeleteGuildRoleInput = typeof DeleteGuildRoleInput.Type; + +// Output Schema +export const DeleteGuildRoleOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteGuildRoleOutput = typeof DeleteGuildRoleOutput.Type; + +// The operation +export const deleteGuildRole = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteGuildRoleInput, + outputSchema: DeleteGuildRoleOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/deleteGuildScheduledEvent.ts b/packages/discord/src/operations/deleteGuildScheduledEvent.ts new file mode 100644 index 000000000..eac0395c9 --- /dev/null +++ b/packages/discord/src/operations/deleteGuildScheduledEvent.ts @@ -0,0 +1,33 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteGuildScheduledEventInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + guild_scheduled_event_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "DELETE", + path: "/guilds/{guild_id}/scheduled-events/{guild_scheduled_event_id}", + }), + ); +export type DeleteGuildScheduledEventInput = + typeof DeleteGuildScheduledEventInput.Type; + +// Output Schema +export const DeleteGuildScheduledEventOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteGuildScheduledEventOutput = + typeof DeleteGuildScheduledEventOutput.Type; + +// The operation +export const deleteGuildScheduledEvent = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: DeleteGuildScheduledEventInput, + outputSchema: DeleteGuildScheduledEventOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/deleteGuildSoundboardSound.ts b/packages/discord/src/operations/deleteGuildSoundboardSound.ts new file mode 100644 index 000000000..6ff1249d6 --- /dev/null +++ b/packages/discord/src/operations/deleteGuildSoundboardSound.ts @@ -0,0 +1,33 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteGuildSoundboardSoundInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + sound_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "DELETE", + path: "/guilds/{guild_id}/soundboard-sounds/{sound_id}", + }), + ); +export type DeleteGuildSoundboardSoundInput = + typeof DeleteGuildSoundboardSoundInput.Type; + +// Output Schema +export const DeleteGuildSoundboardSoundOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteGuildSoundboardSoundOutput = + typeof DeleteGuildSoundboardSoundOutput.Type; + +// The operation +export const deleteGuildSoundboardSound = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: DeleteGuildSoundboardSoundInput, + outputSchema: DeleteGuildSoundboardSoundOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/deleteGuildSticker.ts b/packages/discord/src/operations/deleteGuildSticker.ts new file mode 100644 index 000000000..942050f23 --- /dev/null +++ b/packages/discord/src/operations/deleteGuildSticker.ts @@ -0,0 +1,28 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteGuildStickerInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + sticker_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "DELETE", + path: "/guilds/{guild_id}/stickers/{sticker_id}", + }), + ); +export type DeleteGuildStickerInput = typeof DeleteGuildStickerInput.Type; + +// Output Schema +export const DeleteGuildStickerOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteGuildStickerOutput = typeof DeleteGuildStickerOutput.Type; + +// The operation +export const deleteGuildSticker = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteGuildStickerInput, + outputSchema: DeleteGuildStickerOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/deleteGuildTemplate.ts b/packages/discord/src/operations/deleteGuildTemplate.ts new file mode 100644 index 000000000..126247cd8 --- /dev/null +++ b/packages/discord/src/operations/deleteGuildTemplate.ts @@ -0,0 +1,105 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteGuildTemplateInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + code: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ method: "DELETE", path: "/guilds/{guild_id}/templates/{code}" }), + ); +export type DeleteGuildTemplateInput = typeof DeleteGuildTemplateInput.Type; + +// Output Schema +export const DeleteGuildTemplateOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + code: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + usage_count: Schema.Number, + creator_id: Schema.String, + creator: Schema.Unknown, + created_at: Schema.String, + updated_at: Schema.String, + source_guild_id: Schema.String, + serialized_source_guild: Schema.Struct({ + name: Schema.String, + description: Schema.NullOr(Schema.String), + region: Schema.NullOr(Schema.String), + verification_level: Schema.Unknown, + default_message_notifications: Schema.Unknown, + explicit_content_filter: Schema.Unknown, + preferred_locale: Schema.Unknown, + afk_channel_id: Schema.Unknown, + afk_timeout: Schema.Unknown, + system_channel_id: Schema.Unknown, + system_channel_flags: Schema.Number, + roles: Schema.Array( + Schema.Struct({ + id: Schema.Number, + name: Schema.String, + permissions: Schema.String, + color: Schema.Number, + colors: Schema.Unknown, + hoist: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + }), + ), + channels: Schema.Array( + Schema.Struct({ + id: Schema.NullOr(Schema.Number), + type: Schema.Unknown, + name: Schema.NullOr(Schema.String), + position: Schema.NullOr(Schema.Number), + topic: Schema.NullOr(Schema.String), + bitrate: Schema.Number, + user_limit: Schema.Number, + nsfw: Schema.Boolean, + rate_limit_per_user: Schema.Number, + parent_id: Schema.Unknown, + default_auto_archive_duration: Schema.Unknown, + permission_overwrites: Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + allow: Schema.String, + deny: Schema.String, + }), + ), + available_tags: Schema.NullOr( + Schema.Array( + Schema.Struct({ + id: Schema.NullOr(Schema.Number), + name: Schema.String, + emoji_id: Schema.Unknown, + emoji_name: Schema.NullOr(Schema.String), + moderated: Schema.NullOr(Schema.Boolean), + }), + ), + ), + template: Schema.String, + default_reaction_emoji: Schema.Unknown, + default_thread_rate_limit_per_user: Schema.NullOr(Schema.Number), + default_sort_order: Schema.Unknown, + default_forum_layout: Schema.Unknown, + default_tag_setting: Schema.Unknown, + icon_emoji: Schema.Unknown, + theme_color: Schema.NullOr(Schema.Number), + }), + ), + }), + is_dirty: Schema.NullOr(Schema.Boolean), + }); +export type DeleteGuildTemplateOutput = typeof DeleteGuildTemplateOutput.Type; + +// The operation +export const deleteGuildTemplate = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteGuildTemplateInput, + outputSchema: DeleteGuildTemplateOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/deleteLobbyMember.ts b/packages/discord/src/operations/deleteLobbyMember.ts new file mode 100644 index 000000000..ce195f3d4 --- /dev/null +++ b/packages/discord/src/operations/deleteLobbyMember.ts @@ -0,0 +1,26 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteLobbyMemberInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + lobby_id: Schema.String.pipe(T.PathParam()), + user_id: Schema.String.pipe(T.PathParam()), + }, +).pipe( + T.Http({ method: "DELETE", path: "/lobbies/{lobby_id}/members/{user_id}" }), +); +export type DeleteLobbyMemberInput = typeof DeleteLobbyMemberInput.Type; + +// Output Schema +export const DeleteLobbyMemberOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteLobbyMemberOutput = typeof DeleteLobbyMemberOutput.Type; + +// The operation +export const deleteLobbyMember = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteLobbyMemberInput, + outputSchema: DeleteLobbyMemberOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/deleteMessage.ts b/packages/discord/src/operations/deleteMessage.ts new file mode 100644 index 000000000..ea1ce89fc --- /dev/null +++ b/packages/discord/src/operations/deleteMessage.ts @@ -0,0 +1,27 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteMessageInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), +}).pipe( + T.Http({ + method: "DELETE", + path: "/channels/{channel_id}/messages/{message_id}", + }), +); +export type DeleteMessageInput = typeof DeleteMessageInput.Type; + +// Output Schema +export const DeleteMessageOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteMessageOutput = typeof DeleteMessageOutput.Type; + +// The operation +export const deleteMessage = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteMessageInput, + outputSchema: DeleteMessageOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/deleteMyMessageReaction.ts b/packages/discord/src/operations/deleteMyMessageReaction.ts new file mode 100644 index 000000000..fd81a53c2 --- /dev/null +++ b/packages/discord/src/operations/deleteMyMessageReaction.ts @@ -0,0 +1,34 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteMyMessageReactionInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), + emoji_name: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "DELETE", + path: "/channels/{channel_id}/messages/{message_id}/reactions/{emoji_name}/@me", + }), + ); +export type DeleteMyMessageReactionInput = + typeof DeleteMyMessageReactionInput.Type; + +// Output Schema +export const DeleteMyMessageReactionOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteMyMessageReactionOutput = + typeof DeleteMyMessageReactionOutput.Type; + +// The operation +export const deleteMyMessageReaction = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: DeleteMyMessageReactionInput, + outputSchema: DeleteMyMessageReactionOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/deleteOriginalWebhookMessage.ts b/packages/discord/src/operations/deleteOriginalWebhookMessage.ts new file mode 100644 index 000000000..6d6cd8740 --- /dev/null +++ b/packages/discord/src/operations/deleteOriginalWebhookMessage.ts @@ -0,0 +1,33 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteOriginalWebhookMessageInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + webhook_id: Schema.String.pipe(T.PathParam()), + webhook_token: Schema.String.pipe(T.PathParam()), + thread_id: Schema.optional(Schema.String), + }).pipe( + T.Http({ + method: "DELETE", + path: "/webhooks/{webhook_id}/{webhook_token}/messages/@original", + }), + ); +export type DeleteOriginalWebhookMessageInput = + typeof DeleteOriginalWebhookMessageInput.Type; + +// Output Schema +export const DeleteOriginalWebhookMessageOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteOriginalWebhookMessageOutput = + typeof DeleteOriginalWebhookMessageOutput.Type; + +// The operation +export const deleteOriginalWebhookMessage = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteOriginalWebhookMessageInput, + outputSchema: DeleteOriginalWebhookMessageOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/deletePin.ts b/packages/discord/src/operations/deletePin.ts new file mode 100644 index 000000000..42ed4fd06 --- /dev/null +++ b/packages/discord/src/operations/deletePin.ts @@ -0,0 +1,27 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeletePinInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), +}).pipe( + T.Http({ + method: "DELETE", + path: "/channels/{channel_id}/messages/pins/{message_id}", + }), +); +export type DeletePinInput = typeof DeletePinInput.Type; + +// Output Schema +export const DeletePinOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeletePinOutput = typeof DeletePinOutput.Type; + +// The operation +export const deletePin = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeletePinInput, + outputSchema: DeletePinOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/deleteStageInstance.ts b/packages/discord/src/operations/deleteStageInstance.ts new file mode 100644 index 000000000..badf04dbe --- /dev/null +++ b/packages/discord/src/operations/deleteStageInstance.ts @@ -0,0 +1,23 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteStageInstanceInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + }).pipe(T.Http({ method: "DELETE", path: "/stage-instances/{channel_id}" })); +export type DeleteStageInstanceInput = typeof DeleteStageInstanceInput.Type; + +// Output Schema +export const DeleteStageInstanceOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteStageInstanceOutput = typeof DeleteStageInstanceOutput.Type; + +// The operation +export const deleteStageInstance = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteStageInstanceInput, + outputSchema: DeleteStageInstanceOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/deleteThreadMember.ts b/packages/discord/src/operations/deleteThreadMember.ts new file mode 100644 index 000000000..f76c31592 --- /dev/null +++ b/packages/discord/src/operations/deleteThreadMember.ts @@ -0,0 +1,28 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteThreadMemberInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + user_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "DELETE", + path: "/channels/{channel_id}/thread-members/{user_id}", + }), + ); +export type DeleteThreadMemberInput = typeof DeleteThreadMemberInput.Type; + +// Output Schema +export const DeleteThreadMemberOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteThreadMemberOutput = typeof DeleteThreadMemberOutput.Type; + +// The operation +export const deleteThreadMember = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteThreadMemberInput, + outputSchema: DeleteThreadMemberOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/deleteUserMessageReaction.ts b/packages/discord/src/operations/deleteUserMessageReaction.ts new file mode 100644 index 000000000..2c57b1e77 --- /dev/null +++ b/packages/discord/src/operations/deleteUserMessageReaction.ts @@ -0,0 +1,35 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteUserMessageReactionInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), + emoji_name: Schema.String.pipe(T.PathParam()), + user_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "DELETE", + path: "/channels/{channel_id}/messages/{message_id}/reactions/{emoji_name}/{user_id}", + }), + ); +export type DeleteUserMessageReactionInput = + typeof DeleteUserMessageReactionInput.Type; + +// Output Schema +export const DeleteUserMessageReactionOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteUserMessageReactionOutput = + typeof DeleteUserMessageReactionOutput.Type; + +// The operation +export const deleteUserMessageReaction = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: DeleteUserMessageReactionInput, + outputSchema: DeleteUserMessageReactionOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/deleteWebhook.ts b/packages/discord/src/operations/deleteWebhook.ts new file mode 100644 index 000000000..0cfd175cf --- /dev/null +++ b/packages/discord/src/operations/deleteWebhook.ts @@ -0,0 +1,21 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteWebhookInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + webhook_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "DELETE", path: "/webhooks/{webhook_id}" })); +export type DeleteWebhookInput = typeof DeleteWebhookInput.Type; + +// Output Schema +export const DeleteWebhookOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteWebhookOutput = typeof DeleteWebhookOutput.Type; + +// The operation +export const deleteWebhook = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteWebhookInput, + outputSchema: DeleteWebhookOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/deleteWebhookByToken.ts b/packages/discord/src/operations/deleteWebhookByToken.ts new file mode 100644 index 000000000..b099e63df --- /dev/null +++ b/packages/discord/src/operations/deleteWebhookByToken.ts @@ -0,0 +1,31 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteWebhookByTokenInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + webhook_id: Schema.String.pipe(T.PathParam()), + webhook_token: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "DELETE", + path: "/webhooks/{webhook_id}/{webhook_token}", + }), + ); +export type DeleteWebhookByTokenInput = typeof DeleteWebhookByTokenInput.Type; + +// Output Schema +export const DeleteWebhookByTokenOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteWebhookByTokenOutput = typeof DeleteWebhookByTokenOutput.Type; + +// The operation +export const deleteWebhookByToken = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: DeleteWebhookByTokenInput, + outputSchema: DeleteWebhookByTokenOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/deleteWebhookMessage.ts b/packages/discord/src/operations/deleteWebhookMessage.ts new file mode 100644 index 000000000..ec687ef8a --- /dev/null +++ b/packages/discord/src/operations/deleteWebhookMessage.ts @@ -0,0 +1,33 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteWebhookMessageInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + webhook_id: Schema.String.pipe(T.PathParam()), + webhook_token: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), + thread_id: Schema.optional(Schema.String), + }).pipe( + T.Http({ + method: "DELETE", + path: "/webhooks/{webhook_id}/{webhook_token}/messages/{message_id}", + }), + ); +export type DeleteWebhookMessageInput = typeof DeleteWebhookMessageInput.Type; + +// Output Schema +export const DeleteWebhookMessageOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeleteWebhookMessageOutput = typeof DeleteWebhookMessageOutput.Type; + +// The operation +export const deleteWebhookMessage = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: DeleteWebhookMessageInput, + outputSchema: DeleteWebhookMessageOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/deprecatedCreatePin.ts b/packages/discord/src/operations/deprecatedCreatePin.ts new file mode 100644 index 000000000..968373119 --- /dev/null +++ b/packages/discord/src/operations/deprecatedCreatePin.ts @@ -0,0 +1,26 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeprecatedCreatePinInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ method: "PUT", path: "/channels/{channel_id}/pins/{message_id}" }), + ); +export type DeprecatedCreatePinInput = typeof DeprecatedCreatePinInput.Type; + +// Output Schema +export const DeprecatedCreatePinOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeprecatedCreatePinOutput = typeof DeprecatedCreatePinOutput.Type; + +// The operation +export const deprecatedCreatePin = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeprecatedCreatePinInput, + outputSchema: DeprecatedCreatePinOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/deprecatedDeletePin.ts b/packages/discord/src/operations/deprecatedDeletePin.ts new file mode 100644 index 000000000..6f3a4ab82 --- /dev/null +++ b/packages/discord/src/operations/deprecatedDeletePin.ts @@ -0,0 +1,29 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeprecatedDeletePinInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "DELETE", + path: "/channels/{channel_id}/pins/{message_id}", + }), + ); +export type DeprecatedDeletePinInput = typeof DeprecatedDeletePinInput.Type; + +// Output Schema +export const DeprecatedDeletePinOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type DeprecatedDeletePinOutput = typeof DeprecatedDeletePinOutput.Type; + +// The operation +export const deprecatedDeletePin = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeprecatedDeletePinInput, + outputSchema: DeprecatedDeletePinOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/deprecatedListPins.ts b/packages/discord/src/operations/deprecatedListPins.ts new file mode 100644 index 000000000..c056702fc --- /dev/null +++ b/packages/discord/src/operations/deprecatedListPins.ts @@ -0,0 +1,856 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const DeprecatedListPinsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + }).pipe(T.Http({ method: "GET", path: "/channels/{channel_id}/pins" })); +export type DeprecatedListPinsInput = typeof DeprecatedListPinsInput.Type; + +// Output Schema +export const DeprecatedListPinsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + id: Schema.String, + channel_id: Schema.String, + author: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + pinned: Schema.Boolean, + mention_everyone: Schema.Boolean, + tts: Schema.Boolean, + call: Schema.optional( + Schema.Struct({ + ended_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + participants: Schema.Array(Schema.String), + }), + ), + activity: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + party_id: Schema.optional(Schema.String), + }), + ), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + application_id: Schema.optional(Schema.String), + interaction: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + name_localized: Schema.optional(Schema.String), + }), + ), + nonce: Schema.optional(Schema.Unknown), + webhook_id: Schema.optional(Schema.String), + message_reference: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + channel_id: Schema.String, + message_id: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + }), + ), + thread: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + mention_channels: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + type: Schema.Unknown, + guild_id: Schema.String, + }), + ), + ), + role_subscription_data: Schema.optional( + Schema.Struct({ + role_subscription_listing_id: Schema.String, + tier_name: Schema.String, + total_months_subscribed: Schema.Number, + is_renewal: Schema.Boolean, + }), + ), + purchase_notification: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + guild_product_purchase: Schema.optional( + Schema.Struct({ + listing_id: Schema.String, + product_name: Schema.String, + }), + ), + }), + ), + position: Schema.optional(Schema.Number), + resolved: Schema.optional( + Schema.Struct({ + users: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + ), + members: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + channels: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + ), + roles: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ), + ), + ), + }), + ), + poll: Schema.optional( + Schema.Struct({ + question: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + answers: Schema.Array( + Schema.Struct({ + answer_id: Schema.Number, + poll_media: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + }), + ), + expiry: Schema.String, + allow_multiselect: Schema.Boolean, + layout_type: Schema.Unknown, + results: Schema.Struct({ + answer_counts: Schema.Array( + Schema.Struct({ + id: Schema.Number, + count: Schema.Number, + me_voted: Schema.Boolean, + }), + ), + is_finalized: Schema.Boolean, + }), + }), + ), + shared_client_theme: Schema.optional( + Schema.Struct({ + colors: Schema.Array(Schema.String), + gradient_angle: Schema.Number, + base_mix: Schema.Number, + base_theme: Schema.Unknown, + }), + ), + interaction_metadata: Schema.optional(Schema.Unknown), + message_snapshots: Schema.optional( + Schema.Array( + Schema.Struct({ + message: Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional( + Schema.Unknown, + ), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + }), + }), + ), + ), + reactions: Schema.optional( + Schema.Array( + Schema.Struct({ + emoji: Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + count: Schema.Number, + count_details: Schema.Struct({ + burst: Schema.Number, + normal: Schema.Number, + }), + burst_colors: Schema.Array(Schema.String), + me_burst: Schema.Boolean, + me: Schema.Boolean, + }), + ), + ), + referenced_message: Schema.optional(Schema.Unknown), + }), + ); +export type DeprecatedListPinsOutput = typeof DeprecatedListPinsOutput.Type; + +// The operation +export const deprecatedListPins = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeprecatedListPinsInput, + outputSchema: DeprecatedListPinsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/editLobby.ts b/packages/discord/src/operations/editLobby.ts new file mode 100644 index 000000000..9b768cd79 --- /dev/null +++ b/packages/discord/src/operations/editLobby.ts @@ -0,0 +1,103 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const EditLobbyInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + lobby_id: Schema.String.pipe(T.PathParam()), + idle_timeout_seconds: Schema.optional(Schema.NullOr(Schema.Number)), + metadata: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + members: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + id: Schema.String, + metadata: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + flags: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + flags: Schema.optional(Schema.Unknown), + override_event_webhooks_url: Schema.optional(Schema.NullOr(Schema.String)), +}).pipe(T.Http({ method: "PATCH", path: "/lobbies/{lobby_id}" })); +export type EditLobbyInput = typeof EditLobbyInput.Type; + +// Output Schema +export const EditLobbyOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + application_id: Schema.String, + metadata: Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + members: Schema.Array( + Schema.Struct({ + id: Schema.String, + metadata: Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + flags: Schema.Number, + }), + ), + linked_channel: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + topic: Schema.optional(Schema.NullOr(Schema.String)), + default_auto_archive_duration: Schema.optional(Schema.Unknown), + default_thread_rate_limit_per_user: Schema.optional(Schema.Number), + position: Schema.Number, + permission_overwrites: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + allow: Schema.String, + deny: Schema.String, + }), + ), + ), + nsfw: Schema.optional(Schema.Boolean), + available_tags: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + moderated: Schema.Boolean, + emoji_id: Schema.Unknown, + emoji_name: Schema.NullOr(Schema.String), + }), + ), + ), + default_reaction_emoji: Schema.optional(Schema.Unknown), + default_sort_order: Schema.optional(Schema.Unknown), + default_forum_layout: Schema.optional(Schema.Unknown), + default_tag_setting: Schema.optional(Schema.Unknown), + hd_streaming_until: Schema.optional(Schema.String), + hd_streaming_buyer_id: Schema.optional(Schema.String), + }), + ), + flags: Schema.Number, + override_event_webhooks_url: Schema.optional(Schema.NullOr(Schema.String)), +}); +export type EditLobbyOutput = typeof EditLobbyOutput.Type; + +// The operation +export const editLobby = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: EditLobbyInput, + outputSchema: EditLobbyOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/editLobbyChannelLink.ts b/packages/discord/src/operations/editLobbyChannelLink.ts new file mode 100644 index 000000000..d9eb07168 --- /dev/null +++ b/packages/discord/src/operations/editLobbyChannelLink.ts @@ -0,0 +1,91 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const EditLobbyChannelLinkInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + lobby_id: Schema.String.pipe(T.PathParam()), + channel_id: Schema.optional(Schema.Unknown), + }).pipe( + T.Http({ method: "PATCH", path: "/lobbies/{lobby_id}/channel-linking" }), + ); +export type EditLobbyChannelLinkInput = typeof EditLobbyChannelLinkInput.Type; + +// Output Schema +export const EditLobbyChannelLinkOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + application_id: Schema.String, + metadata: Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + members: Schema.Array( + Schema.Struct({ + id: Schema.String, + metadata: Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + flags: Schema.Number, + }), + ), + linked_channel: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + topic: Schema.optional(Schema.NullOr(Schema.String)), + default_auto_archive_duration: Schema.optional(Schema.Unknown), + default_thread_rate_limit_per_user: Schema.optional(Schema.Number), + position: Schema.Number, + permission_overwrites: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + allow: Schema.String, + deny: Schema.String, + }), + ), + ), + nsfw: Schema.optional(Schema.Boolean), + available_tags: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + moderated: Schema.Boolean, + emoji_id: Schema.Unknown, + emoji_name: Schema.NullOr(Schema.String), + }), + ), + ), + default_reaction_emoji: Schema.optional(Schema.Unknown), + default_sort_order: Schema.optional(Schema.Unknown), + default_forum_layout: Schema.optional(Schema.Unknown), + default_tag_setting: Schema.optional(Schema.Unknown), + hd_streaming_until: Schema.optional(Schema.String), + hd_streaming_buyer_id: Schema.optional(Schema.String), + }), + ), + flags: Schema.Number, + override_event_webhooks_url: Schema.optional(Schema.NullOr(Schema.String)), + }); +export type EditLobbyChannelLinkOutput = typeof EditLobbyChannelLinkOutput.Type; + +// The operation +export const editLobbyChannelLink = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: EditLobbyChannelLinkInput, + outputSchema: EditLobbyChannelLinkOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/executeGithubCompatibleWebhook.ts b/packages/discord/src/operations/executeGithubCompatibleWebhook.ts new file mode 100644 index 000000000..b45bd3f73 --- /dev/null +++ b/packages/discord/src/operations/executeGithubCompatibleWebhook.ts @@ -0,0 +1,73 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ExecuteGithubCompatibleWebhookInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + webhook_id: Schema.String.pipe(T.PathParam()), + webhook_token: Schema.String.pipe(T.PathParam()), + wait: Schema.optional(Schema.Boolean), + thread_id: Schema.optional(Schema.String), + action: Schema.optional(Schema.NullOr(Schema.String)), + ref: Schema.optional(Schema.NullOr(Schema.String)), + ref_type: Schema.optional(Schema.NullOr(Schema.String)), + comment: Schema.optional(Schema.Unknown), + issue: Schema.optional(Schema.Unknown), + pull_request: Schema.optional(Schema.Unknown), + repository: Schema.optional(Schema.Unknown), + forkee: Schema.optional(Schema.Unknown), + sender: Schema.Struct({ + id: Schema.Number, + login: Schema.String, + html_url: Schema.String, + avatar_url: Schema.String, + }), + member: Schema.optional(Schema.Unknown), + release: Schema.optional(Schema.Unknown), + head_commit: Schema.optional(Schema.Unknown), + commits: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + id: Schema.String, + url: Schema.String, + message: Schema.String, + author: Schema.Struct({ + username: Schema.optional(Schema.NullOr(Schema.String)), + name: Schema.String, + }), + }), + ), + ), + ), + forced: Schema.optional(Schema.NullOr(Schema.Boolean)), + compare: Schema.optional(Schema.NullOr(Schema.String)), + review: Schema.optional(Schema.Unknown), + check_run: Schema.optional(Schema.Unknown), + check_suite: Schema.optional(Schema.Unknown), + discussion: Schema.optional(Schema.Unknown), + answer: Schema.optional(Schema.Unknown), + }).pipe( + T.Http({ + method: "POST", + path: "/webhooks/{webhook_id}/{webhook_token}/github", + }), + ); +export type ExecuteGithubCompatibleWebhookInput = + typeof ExecuteGithubCompatibleWebhookInput.Type; + +// Output Schema +export const ExecuteGithubCompatibleWebhookOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type ExecuteGithubCompatibleWebhookOutput = + typeof ExecuteGithubCompatibleWebhookOutput.Type; + +// The operation +export const executeGithubCompatibleWebhook = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ExecuteGithubCompatibleWebhookInput, + outputSchema: ExecuteGithubCompatibleWebhookOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/executeSlackCompatibleWebhook.ts b/packages/discord/src/operations/executeSlackCompatibleWebhook.ts new file mode 100644 index 000000000..3ba808f1d --- /dev/null +++ b/packages/discord/src/operations/executeSlackCompatibleWebhook.ts @@ -0,0 +1,69 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ExecuteSlackCompatibleWebhookInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + webhook_id: Schema.String.pipe(T.PathParam()), + webhook_token: Schema.String.pipe(T.PathParam()), + wait: Schema.optional(Schema.Boolean), + thread_id: Schema.optional(Schema.String), + text: Schema.optional(Schema.NullOr(Schema.String)), + username: Schema.optional(Schema.NullOr(Schema.String)), + icon_url: Schema.optional(Schema.NullOr(Schema.String)), + attachments: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + title: Schema.optional(Schema.NullOr(Schema.String)), + title_link: Schema.optional(Schema.NullOr(Schema.String)), + text: Schema.optional(Schema.NullOr(Schema.String)), + color: Schema.optional(Schema.NullOr(Schema.String)), + ts: Schema.optional(Schema.NullOr(Schema.Number)), + pretext: Schema.optional(Schema.NullOr(Schema.String)), + footer: Schema.optional(Schema.NullOr(Schema.String)), + footer_icon: Schema.optional(Schema.NullOr(Schema.String)), + author_name: Schema.optional(Schema.NullOr(Schema.String)), + author_link: Schema.optional(Schema.NullOr(Schema.String)), + author_icon: Schema.optional(Schema.NullOr(Schema.String)), + image_url: Schema.optional(Schema.NullOr(Schema.String)), + thumb_url: Schema.optional(Schema.NullOr(Schema.String)), + fields: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + name: Schema.optional(Schema.NullOr(Schema.String)), + value: Schema.optional(Schema.NullOr(Schema.String)), + inline: Schema.optional(Schema.NullOr(Schema.Boolean)), + }), + ), + ), + ), + }), + ), + ), + ), + }).pipe( + T.Http({ + method: "POST", + path: "/webhooks/{webhook_id}/{webhook_token}/slack", + }), + ); +export type ExecuteSlackCompatibleWebhookInput = + typeof ExecuteSlackCompatibleWebhookInput.Type; + +// Output Schema +export const ExecuteSlackCompatibleWebhookOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.NullOr(Schema.String); +export type ExecuteSlackCompatibleWebhookOutput = + typeof ExecuteSlackCompatibleWebhookOutput.Type; + +// The operation +export const executeSlackCompatibleWebhook = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ExecuteSlackCompatibleWebhookInput, + outputSchema: ExecuteSlackCompatibleWebhookOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/executeWebhook.ts b/packages/discord/src/operations/executeWebhook.ts new file mode 100644 index 000000000..f1653782a --- /dev/null +++ b/packages/discord/src/operations/executeWebhook.ts @@ -0,0 +1,854 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ExecuteWebhookInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + webhook_id: Schema.String.pipe(T.PathParam()), + webhook_token: Schema.String.pipe(T.PathParam()), + wait: Schema.optional(Schema.Boolean), + thread_id: Schema.optional(Schema.String), + with_components: Schema.optional(Schema.Boolean), +}).pipe( + T.Http({ method: "POST", path: "/webhooks/{webhook_id}/{webhook_token}" }), +); +export type ExecuteWebhookInput = typeof ExecuteWebhookInput.Type; + +// Output Schema +export const ExecuteWebhookOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + id: Schema.String, + channel_id: Schema.String, + author: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + pinned: Schema.Boolean, + mention_everyone: Schema.Boolean, + tts: Schema.Boolean, + call: Schema.optional( + Schema.Struct({ + ended_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + participants: Schema.Array(Schema.String), + }), + ), + activity: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + party_id: Schema.optional(Schema.String), + }), + ), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + application_id: Schema.optional(Schema.String), + interaction: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + name_localized: Schema.optional(Schema.String), + }), + ), + nonce: Schema.optional(Schema.Unknown), + webhook_id: Schema.optional(Schema.String), + message_reference: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + channel_id: Schema.String, + message_id: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + }), + ), + thread: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + mention_channels: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + type: Schema.Unknown, + guild_id: Schema.String, + }), + ), + ), + role_subscription_data: Schema.optional( + Schema.Struct({ + role_subscription_listing_id: Schema.String, + tier_name: Schema.String, + total_months_subscribed: Schema.Number, + is_renewal: Schema.Boolean, + }), + ), + purchase_notification: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + guild_product_purchase: Schema.optional( + Schema.Struct({ + listing_id: Schema.String, + product_name: Schema.String, + }), + ), + }), + ), + position: Schema.optional(Schema.Number), + resolved: Schema.optional( + Schema.Struct({ + users: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + ), + members: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + channels: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + ), + roles: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ), + ), + ), + }), + ), + poll: Schema.optional( + Schema.Struct({ + question: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + answers: Schema.Array( + Schema.Struct({ + answer_id: Schema.Number, + poll_media: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + }), + ), + expiry: Schema.String, + allow_multiselect: Schema.Boolean, + layout_type: Schema.Unknown, + results: Schema.Struct({ + answer_counts: Schema.Array( + Schema.Struct({ + id: Schema.Number, + count: Schema.Number, + me_voted: Schema.Boolean, + }), + ), + is_finalized: Schema.Boolean, + }), + }), + ), + shared_client_theme: Schema.optional( + Schema.Struct({ + colors: Schema.Array(Schema.String), + gradient_angle: Schema.Number, + base_mix: Schema.Number, + base_theme: Schema.Unknown, + }), + ), + interaction_metadata: Schema.optional(Schema.Unknown), + message_snapshots: Schema.optional( + Schema.Array( + Schema.Struct({ + message: Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + }), + }), + ), + ), + reactions: Schema.optional( + Schema.Array( + Schema.Struct({ + emoji: Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + count: Schema.Number, + count_details: Schema.Struct({ + burst: Schema.Number, + normal: Schema.Number, + }), + burst_colors: Schema.Array(Schema.String), + me_burst: Schema.Boolean, + me: Schema.Boolean, + }), + ), + ), + referenced_message: Schema.optional(Schema.Unknown), +}); +export type ExecuteWebhookOutput = typeof ExecuteWebhookOutput.Type; + +// The operation +export const executeWebhook = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ExecuteWebhookInput, + outputSchema: ExecuteWebhookOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/followChannel.ts b/packages/discord/src/operations/followChannel.ts new file mode 100644 index 000000000..dd67defb0 --- /dev/null +++ b/packages/discord/src/operations/followChannel.ts @@ -0,0 +1,25 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const FollowChannelInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + webhook_channel_id: Schema.String, +}).pipe(T.Http({ method: "POST", path: "/channels/{channel_id}/followers" })); +export type FollowChannelInput = typeof FollowChannelInput.Type; + +// Output Schema +export const FollowChannelOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String, + webhook_id: Schema.String, +}); +export type FollowChannelOutput = typeof FollowChannelOutput.Type; + +// The operation +export const followChannel = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: FollowChannelInput, + outputSchema: FollowChannelOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getActiveGuildThreads.ts b/packages/discord/src/operations/getActiveGuildThreads.ts new file mode 100644 index 000000000..23e1698ef --- /dev/null +++ b/packages/discord/src/operations/getActiveGuildThreads.ts @@ -0,0 +1,1005 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetActiveGuildThreadsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + }).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/threads/active" })); +export type GetActiveGuildThreadsInput = typeof GetActiveGuildThreadsInput.Type; + +// Output Schema +export const GetActiveGuildThreadsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + threads: Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + members: Schema.Array( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + has_more: Schema.Boolean, + first_messages: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + id: Schema.String, + channel_id: Schema.String, + author: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + pinned: Schema.Boolean, + mention_everyone: Schema.Boolean, + tts: Schema.Boolean, + call: Schema.optional( + Schema.Struct({ + ended_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + participants: Schema.Array(Schema.String), + }), + ), + activity: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + party_id: Schema.optional(Schema.String), + }), + ), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + application_id: Schema.optional(Schema.String), + interaction: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + name_localized: Schema.optional(Schema.String), + }), + ), + nonce: Schema.optional(Schema.Unknown), + webhook_id: Schema.optional(Schema.String), + message_reference: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + channel_id: Schema.String, + message_id: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + }), + ), + thread: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr( + Schema.String, + ), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + mention_channels: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + type: Schema.Unknown, + guild_id: Schema.String, + }), + ), + ), + role_subscription_data: Schema.optional( + Schema.Struct({ + role_subscription_listing_id: Schema.String, + tier_name: Schema.String, + total_months_subscribed: Schema.Number, + is_renewal: Schema.Boolean, + }), + ), + purchase_notification: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + guild_product_purchase: Schema.optional( + Schema.Struct({ + listing_id: Schema.String, + product_name: Schema.String, + }), + ), + }), + ), + position: Schema.optional(Schema.Number), + resolved: Schema.optional( + Schema.Struct({ + users: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + ), + members: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr( + Schema.String, + ), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + channels: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + ), + roles: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional( + Schema.String, + ), + available_for_purchase: Schema.optional( + Schema.Unknown, + ), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ), + ), + ), + }), + ), + poll: Schema.optional( + Schema.Struct({ + question: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + answers: Schema.Array( + Schema.Struct({ + answer_id: Schema.Number, + poll_media: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + }), + ), + expiry: Schema.String, + allow_multiselect: Schema.Boolean, + layout_type: Schema.Unknown, + results: Schema.Struct({ + answer_counts: Schema.Array( + Schema.Struct({ + id: Schema.Number, + count: Schema.Number, + me_voted: Schema.Boolean, + }), + ), + is_finalized: Schema.Boolean, + }), + }), + ), + shared_client_theme: Schema.optional( + Schema.Struct({ + colors: Schema.Array(Schema.String), + gradient_angle: Schema.Number, + base_mix: Schema.Number, + base_theme: Schema.Unknown, + }), + ), + interaction_metadata: Schema.optional(Schema.Unknown), + message_snapshots: Schema.optional( + Schema.Array( + Schema.Struct({ + message: Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional( + Schema.NullOr(Schema.String), + ), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional( + Schema.Unknown, + ), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional( + Schema.Array(Schema.String), + ), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional( + Schema.Boolean, + ), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional( + Schema.NullOr(Schema.String), + ), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional( + Schema.Unknown, + ), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + }), + }), + ), + ), + reactions: Schema.optional( + Schema.Array( + Schema.Struct({ + emoji: Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + count: Schema.Number, + count_details: Schema.Struct({ + burst: Schema.Number, + normal: Schema.Number, + }), + burst_colors: Schema.Array(Schema.String), + me_burst: Schema.Boolean, + me: Schema.Boolean, + }), + ), + ), + referenced_message: Schema.optional(Schema.Unknown), + }), + ), + ), + }); +export type GetActiveGuildThreadsOutput = + typeof GetActiveGuildThreadsOutput.Type; + +// The operation +export const getActiveGuildThreads = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: GetActiveGuildThreadsInput, + outputSchema: GetActiveGuildThreadsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/getAnswerVoters.ts b/packages/discord/src/operations/getAnswerVoters.ts new file mode 100644 index 000000000..384fa4bc7 --- /dev/null +++ b/packages/discord/src/operations/getAnswerVoters.ts @@ -0,0 +1,49 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetAnswerVotersInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), + answer_id: Schema.Number.pipe(T.PathParam()), + after: Schema.optional(Schema.String), + limit: Schema.optional(Schema.Number), +}).pipe( + T.Http({ + method: "GET", + path: "/channels/{channel_id}/polls/{message_id}/answers/{answer_id}", + }), +); +export type GetAnswerVotersInput = typeof GetAnswerVotersInput.Type; + +// Output Schema +export const GetAnswerVotersOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + users: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), +}); +export type GetAnswerVotersOutput = typeof GetAnswerVotersOutput.Type; + +// The operation +export const getAnswerVoters = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetAnswerVotersInput, + outputSchema: GetAnswerVotersOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getApplication.ts b/packages/discord/src/operations/getApplication.ts new file mode 100644 index 000000000..330f0628d --- /dev/null +++ b/packages/discord/src/operations/getApplication.ts @@ -0,0 +1,106 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetApplicationInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/applications/{application_id}" })); +export type GetApplicationInput = typeof GetApplicationInput.Type; + +// Output Schema +export const GetApplicationOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + redirect_uris: Schema.Array(Schema.String), + interactions_endpoint_url: Schema.NullOr(Schema.String), + role_connections_verification_url: Schema.NullOr(Schema.String), + owner: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + approximate_guild_count: Schema.Number, + approximate_user_install_count: Schema.Number, + approximate_user_authorization_count: Schema.Number, + event_webhooks_url: Schema.optional(Schema.NullOr(Schema.String)), + event_webhooks_status: Schema.optional(Schema.Unknown), + event_webhooks_types: Schema.optional(Schema.Array(Schema.Unknown)), + explicit_content_filter: Schema.Unknown, + team: Schema.Unknown, +}); +export type GetApplicationOutput = typeof GetApplicationOutput.Type; + +// The operation +export const getApplication = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetApplicationInput, + outputSchema: GetApplicationOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getApplicationCommand.ts b/packages/discord/src/operations/getApplicationCommand.ts new file mode 100644 index 000000000..f1df2ea64 --- /dev/null +++ b/packages/discord/src/operations/getApplicationCommand.ts @@ -0,0 +1,54 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetApplicationCommandInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + command_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "GET", + path: "/applications/{application_id}/commands/{command_id}", + }), + ); +export type GetApplicationCommandInput = typeof GetApplicationCommandInput.Type; + +// Output Schema +export const GetApplicationCommandOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + application_id: Schema.String, + version: Schema.String, + default_member_permissions: Schema.NullOr(Schema.String), + type: Schema.Unknown, + name: Schema.String, + name_localized: Schema.optional(Schema.String), + name_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + description: Schema.String, + description_localized: Schema.optional(Schema.String), + description_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + guild_id: Schema.optional(Schema.String), + dm_permission: Schema.optional(Schema.Boolean), + contexts: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + integration_types: Schema.optional(Schema.Array(Schema.Unknown)), + options: Schema.optional(Schema.Array(Schema.Unknown)), + nsfw: Schema.optional(Schema.Boolean), + }); +export type GetApplicationCommandOutput = + typeof GetApplicationCommandOutput.Type; + +// The operation +export const getApplicationCommand = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: GetApplicationCommandInput, + outputSchema: GetApplicationCommandOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/getApplicationEmoji.ts b/packages/discord/src/operations/getApplicationEmoji.ts new file mode 100644 index 000000000..3d91ad3c2 --- /dev/null +++ b/packages/discord/src/operations/getApplicationEmoji.ts @@ -0,0 +1,55 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetApplicationEmojiInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + emoji_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "GET", + path: "/applications/{application_id}/emojis/{emoji_id}", + }), + ); +export type GetApplicationEmojiInput = typeof GetApplicationEmojiInput.Type; + +// Output Schema +export const GetApplicationEmojiOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + roles: Schema.Array(Schema.String), + require_colons: Schema.Boolean, + managed: Schema.Boolean, + animated: Schema.Boolean, + available: Schema.Boolean, + }); +export type GetApplicationEmojiOutput = typeof GetApplicationEmojiOutput.Type; + +// The operation +export const getApplicationEmoji = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetApplicationEmojiInput, + outputSchema: GetApplicationEmojiOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getApplicationRoleConnectionsMetadata.ts b/packages/discord/src/operations/getApplicationRoleConnectionsMetadata.ts new file mode 100644 index 000000000..86beef03e --- /dev/null +++ b/packages/discord/src/operations/getApplicationRoleConnectionsMetadata.ts @@ -0,0 +1,44 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetApplicationRoleConnectionsMetadataInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "GET", + path: "/applications/{application_id}/role-connections/metadata", + }), + ); +export type GetApplicationRoleConnectionsMetadataInput = + typeof GetApplicationRoleConnectionsMetadataInput.Type; + +// Output Schema +export const GetApplicationRoleConnectionsMetadataOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + key: Schema.String, + name: Schema.String, + name_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + description: Schema.String, + description_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + }), + ); +export type GetApplicationRoleConnectionsMetadataOutput = + typeof GetApplicationRoleConnectionsMetadataOutput.Type; + +// The operation +export const getApplicationRoleConnectionsMetadata = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetApplicationRoleConnectionsMetadataInput, + outputSchema: GetApplicationRoleConnectionsMetadataOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/getApplicationUserRoleConnection.ts b/packages/discord/src/operations/getApplicationUserRoleConnection.ts new file mode 100644 index 000000000..2322feb3b --- /dev/null +++ b/packages/discord/src/operations/getApplicationUserRoleConnection.ts @@ -0,0 +1,35 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetApplicationUserRoleConnectionInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "GET", + path: "/users/@me/applications/{application_id}/role-connection", + }), + ); +export type GetApplicationUserRoleConnectionInput = + typeof GetApplicationUserRoleConnectionInput.Type; + +// Output Schema +export const GetApplicationUserRoleConnectionOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + platform_name: Schema.optional(Schema.String), + platform_username: Schema.optional(Schema.NullOr(Schema.String)), + metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), + }); +export type GetApplicationUserRoleConnectionOutput = + typeof GetApplicationUserRoleConnectionOutput.Type; + +// The operation +export const getApplicationUserRoleConnection = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetApplicationUserRoleConnectionInput, + outputSchema: GetApplicationUserRoleConnectionOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/getAutoModerationRule.ts b/packages/discord/src/operations/getAutoModerationRule.ts new file mode 100644 index 000000000..75f2cfd7b --- /dev/null +++ b/packages/discord/src/operations/getAutoModerationRule.ts @@ -0,0 +1,32 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetAutoModerationRuleInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + rule_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "GET", + path: "/guilds/{guild_id}/auto-moderation/rules/{rule_id}", + }), + ); +export type GetAutoModerationRuleInput = typeof GetAutoModerationRuleInput.Type; + +// Output Schema +export const GetAutoModerationRuleOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Unknown; +export type GetAutoModerationRuleOutput = + typeof GetAutoModerationRuleOutput.Type; + +// The operation +export const getAutoModerationRule = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: GetAutoModerationRuleInput, + outputSchema: GetAutoModerationRuleOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/getBotGateway.ts b/packages/discord/src/operations/getBotGateway.ts new file mode 100644 index 000000000..39caca598 --- /dev/null +++ b/packages/discord/src/operations/getBotGateway.ts @@ -0,0 +1,30 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetBotGatewayInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + {}, +).pipe(T.Http({ method: "GET", path: "/gateway/bot" })); +export type GetBotGatewayInput = typeof GetBotGatewayInput.Type; + +// Output Schema +export const GetBotGatewayOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + url: Schema.String, + session_start_limit: Schema.Struct({ + max_concurrency: Schema.Number, + remaining: Schema.Number, + reset_after: Schema.Number, + total: Schema.Number, + }), + shards: Schema.Number, +}); +export type GetBotGatewayOutput = typeof GetBotGatewayOutput.Type; + +// The operation +export const getBotGateway = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetBotGatewayInput, + outputSchema: GetBotGatewayOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getChannel.ts b/packages/discord/src/operations/getChannel.ts new file mode 100644 index 000000000..1aef72b6b --- /dev/null +++ b/packages/discord/src/operations/getChannel.ts @@ -0,0 +1,21 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetChannelInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/channels/{channel_id}" })); +export type GetChannelInput = typeof GetChannelInput.Type; + +// Output Schema +export const GetChannelOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Unknown; +export type GetChannelOutput = typeof GetChannelOutput.Type; + +// The operation +export const getChannel = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetChannelInput, + outputSchema: GetChannelOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getCurrentUserApplicationEntitlements.ts b/packages/discord/src/operations/getCurrentUserApplicationEntitlements.ts new file mode 100644 index 000000000..746ba73d2 --- /dev/null +++ b/packages/discord/src/operations/getCurrentUserApplicationEntitlements.ts @@ -0,0 +1,50 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetCurrentUserApplicationEntitlementsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + sku_ids: Schema.optional(Schema.String), + exclude_consumed: Schema.optional(Schema.Boolean), + }).pipe( + T.Http({ + method: "GET", + path: "/users/@me/applications/{application_id}/entitlements", + }), + ); +export type GetCurrentUserApplicationEntitlementsInput = + typeof GetCurrentUserApplicationEntitlementsInput.Type; + +// Output Schema +export const GetCurrentUserApplicationEntitlementsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + id: Schema.String, + sku_id: Schema.String, + application_id: Schema.String, + user_id: Schema.String, + guild_id: Schema.optional(Schema.Unknown), + deleted: Schema.Boolean, + starts_at: Schema.NullOr(Schema.String), + ends_at: Schema.NullOr(Schema.String), + type: Schema.Unknown, + fulfilled_at: Schema.optional(Schema.NullOr(Schema.String)), + fulfillment_status: Schema.optional(Schema.Unknown), + consumed: Schema.optional(Schema.Boolean), + gifter_user_id: Schema.optional(Schema.Unknown), + parent_id: Schema.optional(Schema.Unknown), + }), + ); +export type GetCurrentUserApplicationEntitlementsOutput = + typeof GetCurrentUserApplicationEntitlementsOutput.Type; + +// The operation +export const getCurrentUserApplicationEntitlements = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetCurrentUserApplicationEntitlementsInput, + outputSchema: GetCurrentUserApplicationEntitlementsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/getEntitlement.ts b/packages/discord/src/operations/getEntitlement.ts new file mode 100644 index 000000000..7372dae9f --- /dev/null +++ b/packages/discord/src/operations/getEntitlement.ts @@ -0,0 +1,42 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetEntitlementInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + entitlement_id: Schema.String.pipe(T.PathParam()), +}).pipe( + T.Http({ + method: "GET", + path: "/applications/{application_id}/entitlements/{entitlement_id}", + }), +); +export type GetEntitlementInput = typeof GetEntitlementInput.Type; + +// Output Schema +export const GetEntitlementOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + sku_id: Schema.String, + application_id: Schema.String, + user_id: Schema.String, + guild_id: Schema.optional(Schema.Unknown), + deleted: Schema.Boolean, + starts_at: Schema.NullOr(Schema.String), + ends_at: Schema.NullOr(Schema.String), + type: Schema.Unknown, + fulfilled_at: Schema.optional(Schema.NullOr(Schema.String)), + fulfillment_status: Schema.optional(Schema.Unknown), + consumed: Schema.optional(Schema.Boolean), + gifter_user_id: Schema.optional(Schema.Unknown), + parent_id: Schema.optional(Schema.Unknown), +}); +export type GetEntitlementOutput = typeof GetEntitlementOutput.Type; + +// The operation +export const getEntitlement = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetEntitlementInput, + outputSchema: GetEntitlementOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getEntitlements.ts b/packages/discord/src/operations/getEntitlements.ts new file mode 100644 index 000000000..a95a2c4d2 --- /dev/null +++ b/packages/discord/src/operations/getEntitlements.ts @@ -0,0 +1,52 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetEntitlementsInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + user_id: Schema.optional(Schema.String), + sku_ids: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), + after: Schema.optional(Schema.String), + limit: Schema.optional(Schema.Number), + exclude_ended: Schema.optional(Schema.Boolean), + exclude_deleted: Schema.optional(Schema.Boolean), + only_active: Schema.optional(Schema.Boolean), +}).pipe( + T.Http({ + method: "GET", + path: "/applications/{application_id}/entitlements", + }), +); +export type GetEntitlementsInput = typeof GetEntitlementsInput.Type; + +// Output Schema +export const GetEntitlementsOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + id: Schema.String, + sku_id: Schema.String, + application_id: Schema.String, + user_id: Schema.String, + guild_id: Schema.optional(Schema.Unknown), + deleted: Schema.Boolean, + starts_at: Schema.NullOr(Schema.String), + ends_at: Schema.NullOr(Schema.String), + type: Schema.Unknown, + fulfilled_at: Schema.optional(Schema.NullOr(Schema.String)), + fulfillment_status: Schema.optional(Schema.Unknown), + consumed: Schema.optional(Schema.Boolean), + gifter_user_id: Schema.optional(Schema.Unknown), + parent_id: Schema.optional(Schema.Unknown), + }), +); +export type GetEntitlementsOutput = typeof GetEntitlementsOutput.Type; + +// The operation +export const getEntitlements = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetEntitlementsInput, + outputSchema: GetEntitlementsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getGateway.ts b/packages/discord/src/operations/getGateway.ts new file mode 100644 index 000000000..13a50aad8 --- /dev/null +++ b/packages/discord/src/operations/getGateway.ts @@ -0,0 +1,23 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGatewayInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + {}, +).pipe(T.Http({ method: "GET", path: "/gateway" })); +export type GetGatewayInput = typeof GetGatewayInput.Type; + +// Output Schema +export const GetGatewayOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + url: Schema.String, +}); +export type GetGatewayOutput = typeof GetGatewayOutput.Type; + +// The operation +export const getGateway = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetGatewayInput, + outputSchema: GetGatewayOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getGuild.ts b/packages/discord/src/operations/getGuild.ts new file mode 100644 index 000000000..deb809b2d --- /dev/null +++ b/packages/discord/src/operations/getGuild.ts @@ -0,0 +1,155 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGuildInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + with_counts: Schema.optional(Schema.Boolean), +}).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}" })); +export type GetGuildInput = typeof GetGuildInput.Type; + +// Output Schema +export const GetGuildOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.NullOr(Schema.String), + home_header: Schema.NullOr(Schema.String), + splash: Schema.NullOr(Schema.String), + discovery_splash: Schema.NullOr(Schema.String), + features: Schema.Array(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + owner_id: Schema.String, + application_id: Schema.Unknown, + region: Schema.String, + afk_channel_id: Schema.Unknown, + afk_timeout: Schema.Unknown, + system_channel_id: Schema.Unknown, + system_channel_flags: Schema.Number, + widget_enabled: Schema.Boolean, + widget_channel_id: Schema.Unknown, + verification_level: Schema.Unknown, + roles: Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ), + default_message_notifications: Schema.Unknown, + mfa_level: Schema.Unknown, + explicit_content_filter: Schema.Unknown, + max_presences: Schema.NullOr(Schema.Number), + max_members: Schema.Number, + max_stage_video_channel_users: Schema.Number, + max_video_channel_users: Schema.Number, + vanity_url_code: Schema.NullOr(Schema.String), + premium_tier: Schema.Unknown, + premium_subscription_count: Schema.Number, + preferred_locale: Schema.Unknown, + rules_channel_id: Schema.Unknown, + safety_alerts_channel_id: Schema.Unknown, + public_updates_channel_id: Schema.Unknown, + premium_progress_bar_enabled: Schema.Boolean, + premium_progress_bar_enabled_user_updated_at: Schema.optional( + Schema.NullOr(Schema.String), + ), + nsfw: Schema.Boolean, + nsfw_level: Schema.Unknown, + emojis: Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + roles: Schema.Array(Schema.String), + require_colons: Schema.Boolean, + managed: Schema.Boolean, + animated: Schema.Boolean, + available: Schema.Boolean, + }), + ), + stickers: Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + tags: Schema.String, + type: Schema.Unknown, + format_type: Schema.Unknown, + description: Schema.NullOr(Schema.String), + available: Schema.Boolean, + guild_id: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + incidents_data: Schema.Unknown, + approximate_member_count: Schema.optional(Schema.NullOr(Schema.Number)), + approximate_presence_count: Schema.optional(Schema.NullOr(Schema.Number)), +}); +export type GetGuildOutput = typeof GetGuildOutput.Type; + +// The operation +export const getGuild = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetGuildInput, + outputSchema: GetGuildOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getGuildApplicationCommand.ts b/packages/discord/src/operations/getGuildApplicationCommand.ts new file mode 100644 index 000000000..19de1a9aa --- /dev/null +++ b/packages/discord/src/operations/getGuildApplicationCommand.ts @@ -0,0 +1,56 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGuildApplicationCommandInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + guild_id: Schema.String.pipe(T.PathParam()), + command_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "GET", + path: "/applications/{application_id}/guilds/{guild_id}/commands/{command_id}", + }), + ); +export type GetGuildApplicationCommandInput = + typeof GetGuildApplicationCommandInput.Type; + +// Output Schema +export const GetGuildApplicationCommandOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + application_id: Schema.String, + version: Schema.String, + default_member_permissions: Schema.NullOr(Schema.String), + type: Schema.Unknown, + name: Schema.String, + name_localized: Schema.optional(Schema.String), + name_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + description: Schema.String, + description_localized: Schema.optional(Schema.String), + description_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + guild_id: Schema.optional(Schema.String), + dm_permission: Schema.optional(Schema.Boolean), + contexts: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + integration_types: Schema.optional(Schema.Array(Schema.Unknown)), + options: Schema.optional(Schema.Array(Schema.Unknown)), + nsfw: Schema.optional(Schema.Boolean), + }); +export type GetGuildApplicationCommandOutput = + typeof GetGuildApplicationCommandOutput.Type; + +// The operation +export const getGuildApplicationCommand = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: GetGuildApplicationCommandInput, + outputSchema: GetGuildApplicationCommandOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/getGuildApplicationCommandPermissions.ts b/packages/discord/src/operations/getGuildApplicationCommandPermissions.ts new file mode 100644 index 000000000..7e1ee0808 --- /dev/null +++ b/packages/discord/src/operations/getGuildApplicationCommandPermissions.ts @@ -0,0 +1,44 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGuildApplicationCommandPermissionsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + guild_id: Schema.String.pipe(T.PathParam()), + command_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "GET", + path: "/applications/{application_id}/guilds/{guild_id}/commands/{command_id}/permissions", + }), + ); +export type GetGuildApplicationCommandPermissionsInput = + typeof GetGuildApplicationCommandPermissionsInput.Type; + +// Output Schema +export const GetGuildApplicationCommandPermissionsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + application_id: Schema.String, + guild_id: Schema.String, + permissions: Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + permission: Schema.Boolean, + }), + ), + }); +export type GetGuildApplicationCommandPermissionsOutput = + typeof GetGuildApplicationCommandPermissionsOutput.Type; + +// The operation +export const getGuildApplicationCommandPermissions = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetGuildApplicationCommandPermissionsInput, + outputSchema: GetGuildApplicationCommandPermissionsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/getGuildBan.ts b/packages/discord/src/operations/getGuildBan.ts new file mode 100644 index 000000000..9e585b9ad --- /dev/null +++ b/packages/discord/src/operations/getGuildBan.ts @@ -0,0 +1,40 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGuildBanInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + user_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/bans/{user_id}" })); +export type GetGuildBanInput = typeof GetGuildBanInput.Type; + +// Output Schema +export const GetGuildBanOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + reason: Schema.NullOr(Schema.String), +}); +export type GetGuildBanOutput = typeof GetGuildBanOutput.Type; + +// The operation +export const getGuildBan = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetGuildBanInput, + outputSchema: GetGuildBanOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getGuildEmoji.ts b/packages/discord/src/operations/getGuildEmoji.ts new file mode 100644 index 000000000..e9cce6c21 --- /dev/null +++ b/packages/discord/src/operations/getGuildEmoji.ts @@ -0,0 +1,50 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGuildEmojiInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + emoji_id: Schema.String.pipe(T.PathParam()), +}).pipe( + T.Http({ method: "GET", path: "/guilds/{guild_id}/emojis/{emoji_id}" }), +); +export type GetGuildEmojiInput = typeof GetGuildEmojiInput.Type; + +// Output Schema +export const GetGuildEmojiOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + roles: Schema.Array(Schema.String), + require_colons: Schema.Boolean, + managed: Schema.Boolean, + animated: Schema.Boolean, + available: Schema.Boolean, +}); +export type GetGuildEmojiOutput = typeof GetGuildEmojiOutput.Type; + +// The operation +export const getGuildEmoji = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetGuildEmojiInput, + outputSchema: GetGuildEmojiOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getGuildJoinRequests.ts b/packages/discord/src/operations/getGuildJoinRequests.ts new file mode 100644 index 000000000..32ec4d98c --- /dev/null +++ b/packages/discord/src/operations/getGuildJoinRequests.ts @@ -0,0 +1,52 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGuildJoinRequestsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + status: Schema.optional(Schema.String), + limit: Schema.optional(Schema.Number), + before: Schema.optional(Schema.String), + after: Schema.optional(Schema.String), + }).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/requests" })); +export type GetGuildJoinRequestsInput = typeof GetGuildJoinRequestsInput.Type; + +// Output Schema +export const GetGuildJoinRequestsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + total: Schema.optional(Schema.Number), + guild_join_requests: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + created_at: Schema.String, + reviewed_at: Schema.NullOr(Schema.String), + application_status: Schema.Unknown, + rejection_reason: Schema.NullOr(Schema.String), + guild_id: Schema.String, + user_id: Schema.String, + user: Schema.optional(Schema.Unknown), + form_responses: Schema.optional( + Schema.NullOr(Schema.Array(Schema.Unknown)), + ), + actioned_by_user: Schema.optional(Schema.Unknown), + }), + ), + ), + }); +export type GetGuildJoinRequestsOutput = typeof GetGuildJoinRequestsOutput.Type; + +// The operation +/** + * List join requests for guild, optionally filtered by application status + */ +export const getGuildJoinRequests = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: GetGuildJoinRequestsInput, + outputSchema: GetGuildJoinRequestsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/getGuildMember.ts b/packages/discord/src/operations/getGuildMember.ts new file mode 100644 index 000000000..2291637c3 --- /dev/null +++ b/packages/discord/src/operations/getGuildMember.ts @@ -0,0 +1,54 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGuildMemberInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + user_id: Schema.String.pipe(T.PathParam()), +}).pipe( + T.Http({ method: "GET", path: "/guilds/{guild_id}/members/{user_id}" }), +); +export type GetGuildMemberInput = typeof GetGuildMemberInput.Type; + +// Output Schema +export const GetGuildMemberOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, +}); +export type GetGuildMemberOutput = typeof GetGuildMemberOutput.Type; + +// The operation +export const getGuildMember = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetGuildMemberInput, + outputSchema: GetGuildMemberOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getGuildNewMemberWelcome.ts b/packages/discord/src/operations/getGuildNewMemberWelcome.ts new file mode 100644 index 000000000..029c147cb --- /dev/null +++ b/packages/discord/src/operations/getGuildNewMemberWelcome.ts @@ -0,0 +1,69 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGuildNewMemberWelcomeInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ method: "GET", path: "/guilds/{guild_id}/new-member-welcome" }), + ); +export type GetGuildNewMemberWelcomeInput = + typeof GetGuildNewMemberWelcomeInput.Type; + +// Output Schema +export const GetGuildNewMemberWelcomeOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String, + enabled: Schema.Boolean, + welcome_message: Schema.optional( + Schema.Struct({ + author_ids: Schema.Array(Schema.String), + message: Schema.String, + }), + ), + new_member_actions: Schema.Array( + Schema.Struct({ + channel_id: Schema.String, + action_type: Schema.Unknown, + title: Schema.String, + description: Schema.String, + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.Boolean, + }), + ), + icon: Schema.optional(Schema.String), + }), + ), + resource_channels: Schema.Array( + Schema.Struct({ + channel_id: Schema.String, + title: Schema.String, + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.Boolean, + }), + ), + icon: Schema.optional(Schema.String), + description: Schema.String, + }), + ), + }); +export type GetGuildNewMemberWelcomeOutput = + typeof GetGuildNewMemberWelcomeOutput.Type; + +// The operation +export const getGuildNewMemberWelcome = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: GetGuildNewMemberWelcomeInput, + outputSchema: GetGuildNewMemberWelcomeOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/getGuildPreview.ts b/packages/discord/src/operations/getGuildPreview.ts new file mode 100644 index 000000000..8f7de8a13 --- /dev/null +++ b/packages/discord/src/operations/getGuildPreview.ts @@ -0,0 +1,91 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGuildPreviewInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/preview" })); +export type GetGuildPreviewInput = typeof GetGuildPreviewInput.Type; + +// Output Schema +export const GetGuildPreviewOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.NullOr(Schema.String), + home_header: Schema.NullOr(Schema.String), + splash: Schema.NullOr(Schema.String), + discovery_splash: Schema.NullOr(Schema.String), + features: Schema.Array(Schema.Unknown), + approximate_member_count: Schema.Number, + approximate_presence_count: Schema.Number, + emojis: Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + roles: Schema.Array(Schema.String), + require_colons: Schema.Boolean, + managed: Schema.Boolean, + animated: Schema.Boolean, + available: Schema.Boolean, + }), + ), + stickers: Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + tags: Schema.String, + type: Schema.Unknown, + format_type: Schema.Unknown, + description: Schema.NullOr(Schema.String), + available: Schema.Boolean, + guild_id: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), +}); +export type GetGuildPreviewOutput = typeof GetGuildPreviewOutput.Type; + +// The operation +export const getGuildPreview = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetGuildPreviewInput, + outputSchema: GetGuildPreviewOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getGuildRole.ts b/packages/discord/src/operations/getGuildRole.ts new file mode 100644 index 000000000..859a314e7 --- /dev/null +++ b/packages/discord/src/operations/getGuildRole.ts @@ -0,0 +1,50 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGuildRoleInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + role_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/roles/{role_id}" })); +export type GetGuildRoleInput = typeof GetGuildRoleInput.Type; + +// Output Schema +export const GetGuildRoleOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, +}); +export type GetGuildRoleOutput = typeof GetGuildRoleOutput.Type; + +// The operation +export const getGuildRole = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetGuildRoleInput, + outputSchema: GetGuildRoleOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getGuildScheduledEvent.ts b/packages/discord/src/operations/getGuildScheduledEvent.ts new file mode 100644 index 000000000..31bd4b984 --- /dev/null +++ b/packages/discord/src/operations/getGuildScheduledEvent.ts @@ -0,0 +1,34 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGuildScheduledEventInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + guild_scheduled_event_id: Schema.String.pipe(T.PathParam()), + with_user_count: Schema.optional(Schema.Boolean), + }).pipe( + T.Http({ + method: "GET", + path: "/guilds/{guild_id}/scheduled-events/{guild_scheduled_event_id}", + }), + ); +export type GetGuildScheduledEventInput = + typeof GetGuildScheduledEventInput.Type; + +// Output Schema +export const GetGuildScheduledEventOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Unknown; +export type GetGuildScheduledEventOutput = + typeof GetGuildScheduledEventOutput.Type; + +// The operation +export const getGuildScheduledEvent = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: GetGuildScheduledEventInput, + outputSchema: GetGuildScheduledEventOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/getGuildSoundboardSound.ts b/packages/discord/src/operations/getGuildSoundboardSound.ts new file mode 100644 index 000000000..5727df1fc --- /dev/null +++ b/packages/discord/src/operations/getGuildSoundboardSound.ts @@ -0,0 +1,59 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGuildSoundboardSoundInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + sound_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "GET", + path: "/guilds/{guild_id}/soundboard-sounds/{sound_id}", + }), + ); +export type GetGuildSoundboardSoundInput = + typeof GetGuildSoundboardSoundInput.Type; + +// Output Schema +export const GetGuildSoundboardSoundOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + name: Schema.String, + sound_id: Schema.String, + volume: Schema.Number, + emoji_id: Schema.Unknown, + emoji_name: Schema.NullOr(Schema.String), + guild_id: Schema.optional(Schema.String), + available: Schema.Boolean, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }); +export type GetGuildSoundboardSoundOutput = + typeof GetGuildSoundboardSoundOutput.Type; + +// The operation +export const getGuildSoundboardSound = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: GetGuildSoundboardSoundInput, + outputSchema: GetGuildSoundboardSoundOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/getGuildSticker.ts b/packages/discord/src/operations/getGuildSticker.ts new file mode 100644 index 000000000..f3754cdc9 --- /dev/null +++ b/packages/discord/src/operations/getGuildSticker.ts @@ -0,0 +1,51 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGuildStickerInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + sticker_id: Schema.String.pipe(T.PathParam()), +}).pipe( + T.Http({ method: "GET", path: "/guilds/{guild_id}/stickers/{sticker_id}" }), +); +export type GetGuildStickerInput = typeof GetGuildStickerInput.Type; + +// Output Schema +export const GetGuildStickerOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + name: Schema.String, + tags: Schema.String, + type: Schema.Unknown, + format_type: Schema.Unknown, + description: Schema.NullOr(Schema.String), + available: Schema.Boolean, + guild_id: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), +}); +export type GetGuildStickerOutput = typeof GetGuildStickerOutput.Type; + +// The operation +export const getGuildSticker = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetGuildStickerInput, + outputSchema: GetGuildStickerOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getGuildTemplate.ts b/packages/discord/src/operations/getGuildTemplate.ts new file mode 100644 index 000000000..06f5cdc49 --- /dev/null +++ b/packages/discord/src/operations/getGuildTemplate.ts @@ -0,0 +1,102 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGuildTemplateInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + code: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/guilds/templates/{code}" })); +export type GetGuildTemplateInput = typeof GetGuildTemplateInput.Type; + +// Output Schema +export const GetGuildTemplateOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + code: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + usage_count: Schema.Number, + creator_id: Schema.String, + creator: Schema.Unknown, + created_at: Schema.String, + updated_at: Schema.String, + source_guild_id: Schema.String, + serialized_source_guild: Schema.Struct({ + name: Schema.String, + description: Schema.NullOr(Schema.String), + region: Schema.NullOr(Schema.String), + verification_level: Schema.Unknown, + default_message_notifications: Schema.Unknown, + explicit_content_filter: Schema.Unknown, + preferred_locale: Schema.Unknown, + afk_channel_id: Schema.Unknown, + afk_timeout: Schema.Unknown, + system_channel_id: Schema.Unknown, + system_channel_flags: Schema.Number, + roles: Schema.Array( + Schema.Struct({ + id: Schema.Number, + name: Schema.String, + permissions: Schema.String, + color: Schema.Number, + colors: Schema.Unknown, + hoist: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + }), + ), + channels: Schema.Array( + Schema.Struct({ + id: Schema.NullOr(Schema.Number), + type: Schema.Unknown, + name: Schema.NullOr(Schema.String), + position: Schema.NullOr(Schema.Number), + topic: Schema.NullOr(Schema.String), + bitrate: Schema.Number, + user_limit: Schema.Number, + nsfw: Schema.Boolean, + rate_limit_per_user: Schema.Number, + parent_id: Schema.Unknown, + default_auto_archive_duration: Schema.Unknown, + permission_overwrites: Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + allow: Schema.String, + deny: Schema.String, + }), + ), + available_tags: Schema.NullOr( + Schema.Array( + Schema.Struct({ + id: Schema.NullOr(Schema.Number), + name: Schema.String, + emoji_id: Schema.Unknown, + emoji_name: Schema.NullOr(Schema.String), + moderated: Schema.NullOr(Schema.Boolean), + }), + ), + ), + template: Schema.String, + default_reaction_emoji: Schema.Unknown, + default_thread_rate_limit_per_user: Schema.NullOr(Schema.Number), + default_sort_order: Schema.Unknown, + default_forum_layout: Schema.Unknown, + default_tag_setting: Schema.Unknown, + icon_emoji: Schema.Unknown, + theme_color: Schema.NullOr(Schema.Number), + }), + ), + }), + is_dirty: Schema.NullOr(Schema.Boolean), + }, +); +export type GetGuildTemplateOutput = typeof GetGuildTemplateOutput.Type; + +// The operation +export const getGuildTemplate = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetGuildTemplateInput, + outputSchema: GetGuildTemplateOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getGuildVanityUrl.ts b/packages/discord/src/operations/getGuildVanityUrl.ts new file mode 100644 index 000000000..3b229a131 --- /dev/null +++ b/packages/discord/src/operations/getGuildVanityUrl.ts @@ -0,0 +1,28 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGuildVanityUrlInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + guild_id: Schema.String.pipe(T.PathParam()), + }, +).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/vanity-url" })); +export type GetGuildVanityUrlInput = typeof GetGuildVanityUrlInput.Type; + +// Output Schema +export const GetGuildVanityUrlOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + code: Schema.NullOr(Schema.String), + uses: Schema.Number, + error: Schema.optional(Schema.Unknown), + }); +export type GetGuildVanityUrlOutput = typeof GetGuildVanityUrlOutput.Type; + +// The operation +export const getGuildVanityUrl = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetGuildVanityUrlInput, + outputSchema: GetGuildVanityUrlOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getGuildWebhooks.ts b/packages/discord/src/operations/getGuildWebhooks.ts new file mode 100644 index 000000000..375da01a3 --- /dev/null +++ b/packages/discord/src/operations/getGuildWebhooks.ts @@ -0,0 +1,23 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGuildWebhooksInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/webhooks" })); +export type GetGuildWebhooksInput = typeof GetGuildWebhooksInput.Type; + +// Output Schema +export const GetGuildWebhooksOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Unknown, +); +export type GetGuildWebhooksOutput = typeof GetGuildWebhooksOutput.Type; + +// The operation +export const getGuildWebhooks = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetGuildWebhooksInput, + outputSchema: GetGuildWebhooksOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getGuildWelcomeScreen.ts b/packages/discord/src/operations/getGuildWelcomeScreen.ts new file mode 100644 index 000000000..3499fe65a --- /dev/null +++ b/packages/discord/src/operations/getGuildWelcomeScreen.ts @@ -0,0 +1,36 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGuildWelcomeScreenInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + }).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/welcome-screen" })); +export type GetGuildWelcomeScreenInput = typeof GetGuildWelcomeScreenInput.Type; + +// Output Schema +export const GetGuildWelcomeScreenOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + description: Schema.NullOr(Schema.String), + welcome_channels: Schema.Array( + Schema.Struct({ + channel_id: Schema.String, + description: Schema.String, + emoji_id: Schema.Unknown, + emoji_name: Schema.NullOr(Schema.String), + }), + ), + }); +export type GetGuildWelcomeScreenOutput = + typeof GetGuildWelcomeScreenOutput.Type; + +// The operation +export const getGuildWelcomeScreen = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: GetGuildWelcomeScreenInput, + outputSchema: GetGuildWelcomeScreenOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/getGuildWidget.ts b/packages/discord/src/operations/getGuildWidget.ts new file mode 100644 index 000000000..897b86a6b --- /dev/null +++ b/packages/discord/src/operations/getGuildWidget.ts @@ -0,0 +1,54 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGuildWidgetInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/widget.json" })); +export type GetGuildWidgetInput = typeof GetGuildWidgetInput.Type; + +// Output Schema +export const GetGuildWidgetOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + name: Schema.String, + instant_invite: Schema.NullOr(Schema.String), + channels: Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + position: Schema.Number, + }), + ), + members: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + discriminator: Schema.Unknown, + avatar: Schema.Unknown, + status: Schema.String, + avatar_url: Schema.String, + activity: Schema.optional( + Schema.Struct({ + name: Schema.String, + }), + ), + deaf: Schema.optional(Schema.Boolean), + mute: Schema.optional(Schema.Boolean), + self_deaf: Schema.optional(Schema.Boolean), + self_mute: Schema.optional(Schema.Boolean), + suppress: Schema.optional(Schema.Boolean), + channel_id: Schema.optional(Schema.String), + }), + ), + presence_count: Schema.Number, +}); +export type GetGuildWidgetOutput = typeof GetGuildWidgetOutput.Type; + +// The operation +export const getGuildWidget = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetGuildWidgetInput, + outputSchema: GetGuildWidgetOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getGuildWidgetPng.ts b/packages/discord/src/operations/getGuildWidgetPng.ts new file mode 100644 index 000000000..170d60c4d --- /dev/null +++ b/packages/discord/src/operations/getGuildWidgetPng.ts @@ -0,0 +1,24 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGuildWidgetPngInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + guild_id: Schema.String.pipe(T.PathParam()), + style: Schema.optional(Schema.String), + }, +).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/widget.png" })); +export type GetGuildWidgetPngInput = typeof GetGuildWidgetPngInput.Type; + +// Output Schema +export const GetGuildWidgetPngOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type GetGuildWidgetPngOutput = typeof GetGuildWidgetPngOutput.Type; + +// The operation +export const getGuildWidgetPng = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetGuildWidgetPngInput, + outputSchema: GetGuildWidgetPngOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getGuildWidgetSettings.ts b/packages/discord/src/operations/getGuildWidgetSettings.ts new file mode 100644 index 000000000..f94d8ca86 --- /dev/null +++ b/packages/discord/src/operations/getGuildWidgetSettings.ts @@ -0,0 +1,30 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGuildWidgetSettingsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + }).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/widget" })); +export type GetGuildWidgetSettingsInput = + typeof GetGuildWidgetSettingsInput.Type; + +// Output Schema +export const GetGuildWidgetSettingsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + enabled: Schema.Boolean, + channel_id: Schema.Unknown, + }); +export type GetGuildWidgetSettingsOutput = + typeof GetGuildWidgetSettingsOutput.Type; + +// The operation +export const getGuildWidgetSettings = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: GetGuildWidgetSettingsInput, + outputSchema: GetGuildWidgetSettingsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/getGuildsOnboarding.ts b/packages/discord/src/operations/getGuildsOnboarding.ts new file mode 100644 index 000000000..957342aad --- /dev/null +++ b/packages/discord/src/operations/getGuildsOnboarding.ts @@ -0,0 +1,52 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetGuildsOnboardingInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + }).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/onboarding" })); +export type GetGuildsOnboardingInput = typeof GetGuildsOnboardingInput.Type; + +// Output Schema +export const GetGuildsOnboardingOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String, + prompts: Schema.Array( + Schema.Struct({ + id: Schema.String, + title: Schema.String, + options: Schema.Array( + Schema.Struct({ + id: Schema.String, + title: Schema.String, + description: Schema.String, + emoji: Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.Boolean, + }), + role_ids: Schema.Array(Schema.String), + channel_ids: Schema.Array(Schema.String), + }), + ), + single_select: Schema.Boolean, + required: Schema.Boolean, + in_onboarding: Schema.Boolean, + type: Schema.Unknown, + }), + ), + default_channel_ids: Schema.Array(Schema.String), + enabled: Schema.Boolean, + mode: Schema.Unknown, + }); +export type GetGuildsOnboardingOutput = typeof GetGuildsOnboardingOutput.Type; + +// The operation +export const getGuildsOnboarding = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetGuildsOnboardingInput, + outputSchema: GetGuildsOnboardingOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getInviteTargetUsers.ts b/packages/discord/src/operations/getInviteTargetUsers.ts new file mode 100644 index 000000000..fbbf853b1 --- /dev/null +++ b/packages/discord/src/operations/getInviteTargetUsers.ts @@ -0,0 +1,28 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetInviteTargetUsersInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + code: Schema.String.pipe(T.PathParam()), + }).pipe(T.Http({ method: "GET", path: "/invites/{code}/target-users" })); +export type GetInviteTargetUsersInput = typeof GetInviteTargetUsersInput.Type; + +// Output Schema +export const GetInviteTargetUsersOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type GetInviteTargetUsersOutput = typeof GetInviteTargetUsersOutput.Type; + +// The operation +/** + * Get the target users for an invite. + */ +export const getInviteTargetUsers = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: GetInviteTargetUsersInput, + outputSchema: GetInviteTargetUsersOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/getInviteTargetUsersJobStatus.ts b/packages/discord/src/operations/getInviteTargetUsersJobStatus.ts new file mode 100644 index 000000000..0f55b9f7b --- /dev/null +++ b/packages/discord/src/operations/getInviteTargetUsersJobStatus.ts @@ -0,0 +1,38 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetInviteTargetUsersJobStatusInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + code: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ method: "GET", path: "/invites/{code}/target-users/job-status" }), + ); +export type GetInviteTargetUsersJobStatusInput = + typeof GetInviteTargetUsersJobStatusInput.Type; + +// Output Schema +export const GetInviteTargetUsersJobStatusOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + status: Schema.Unknown, + total_users: Schema.Number, + processed_users: Schema.Number, + created_at: Schema.NullOr(Schema.String), + completed_at: Schema.NullOr(Schema.String), + error_message: Schema.NullOr(Schema.String), + }); +export type GetInviteTargetUsersJobStatusOutput = + typeof GetInviteTargetUsersJobStatusOutput.Type; + +// The operation +/** + * Get the target users job status for an invite. + */ +export const getInviteTargetUsersJobStatus = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetInviteTargetUsersJobStatusInput, + outputSchema: GetInviteTargetUsersJobStatusOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/getLobby.ts b/packages/discord/src/operations/getLobby.ts new file mode 100644 index 000000000..19038ddff --- /dev/null +++ b/packages/discord/src/operations/getLobby.ts @@ -0,0 +1,84 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetLobbyInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + lobby_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/lobbies/{lobby_id}" })); +export type GetLobbyInput = typeof GetLobbyInput.Type; + +// Output Schema +export const GetLobbyOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + application_id: Schema.String, + metadata: Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + members: Schema.Array( + Schema.Struct({ + id: Schema.String, + metadata: Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + flags: Schema.Number, + }), + ), + linked_channel: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + topic: Schema.optional(Schema.NullOr(Schema.String)), + default_auto_archive_duration: Schema.optional(Schema.Unknown), + default_thread_rate_limit_per_user: Schema.optional(Schema.Number), + position: Schema.Number, + permission_overwrites: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + allow: Schema.String, + deny: Schema.String, + }), + ), + ), + nsfw: Schema.optional(Schema.Boolean), + available_tags: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + moderated: Schema.Boolean, + emoji_id: Schema.Unknown, + emoji_name: Schema.NullOr(Schema.String), + }), + ), + ), + default_reaction_emoji: Schema.optional(Schema.Unknown), + default_sort_order: Schema.optional(Schema.Unknown), + default_forum_layout: Schema.optional(Schema.Unknown), + default_tag_setting: Schema.optional(Schema.Unknown), + hd_streaming_until: Schema.optional(Schema.String), + hd_streaming_buyer_id: Schema.optional(Schema.String), + }), + ), + flags: Schema.Number, + override_event_webhooks_url: Schema.optional(Schema.NullOr(Schema.String)), +}); +export type GetLobbyOutput = typeof GetLobbyOutput.Type; + +// The operation +export const getLobby = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetLobbyInput, + outputSchema: GetLobbyOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getLobbyMessages.ts b/packages/discord/src/operations/getLobbyMessages.ts new file mode 100644 index 000000000..9207887c9 --- /dev/null +++ b/packages/discord/src/operations/getLobbyMessages.ts @@ -0,0 +1,52 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetLobbyMessagesInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + lobby_id: Schema.String.pipe(T.PathParam()), + limit: Schema.optional(Schema.Number), +}).pipe(T.Http({ method: "GET", path: "/lobbies/{lobby_id}/messages" })); +export type GetLobbyMessagesInput = typeof GetLobbyMessagesInput.Type; + +// Output Schema +export const GetLobbyMessagesOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + content: Schema.String, + lobby_id: Schema.String, + channel_id: Schema.String, + author: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), + moderation_metadata: Schema.optional( + Schema.Record(Schema.String, Schema.String), + ), + flags: Schema.Number, + application_id: Schema.optional(Schema.String), + }), +); +export type GetLobbyMessagesOutput = typeof GetLobbyMessagesOutput.Type; + +// The operation +export const getLobbyMessages = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetLobbyMessagesInput, + outputSchema: GetLobbyMessagesOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getMessage.ts b/packages/discord/src/operations/getMessage.ts new file mode 100644 index 000000000..d59296563 --- /dev/null +++ b/packages/discord/src/operations/getMessage.ts @@ -0,0 +1,854 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetMessageInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), +}).pipe( + T.Http({ + method: "GET", + path: "/channels/{channel_id}/messages/{message_id}", + }), +); +export type GetMessageInput = typeof GetMessageInput.Type; + +// Output Schema +export const GetMessageOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + id: Schema.String, + channel_id: Schema.String, + author: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + pinned: Schema.Boolean, + mention_everyone: Schema.Boolean, + tts: Schema.Boolean, + call: Schema.optional( + Schema.Struct({ + ended_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + participants: Schema.Array(Schema.String), + }), + ), + activity: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + party_id: Schema.optional(Schema.String), + }), + ), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + application_id: Schema.optional(Schema.String), + interaction: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + name_localized: Schema.optional(Schema.String), + }), + ), + nonce: Schema.optional(Schema.Unknown), + webhook_id: Schema.optional(Schema.String), + message_reference: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + channel_id: Schema.String, + message_id: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + }), + ), + thread: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + mention_channels: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + type: Schema.Unknown, + guild_id: Schema.String, + }), + ), + ), + role_subscription_data: Schema.optional( + Schema.Struct({ + role_subscription_listing_id: Schema.String, + tier_name: Schema.String, + total_months_subscribed: Schema.Number, + is_renewal: Schema.Boolean, + }), + ), + purchase_notification: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + guild_product_purchase: Schema.optional( + Schema.Struct({ + listing_id: Schema.String, + product_name: Schema.String, + }), + ), + }), + ), + position: Schema.optional(Schema.Number), + resolved: Schema.optional( + Schema.Struct({ + users: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + ), + members: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + channels: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + ), + roles: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ), + ), + ), + }), + ), + poll: Schema.optional( + Schema.Struct({ + question: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + answers: Schema.Array( + Schema.Struct({ + answer_id: Schema.Number, + poll_media: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + }), + ), + expiry: Schema.String, + allow_multiselect: Schema.Boolean, + layout_type: Schema.Unknown, + results: Schema.Struct({ + answer_counts: Schema.Array( + Schema.Struct({ + id: Schema.Number, + count: Schema.Number, + me_voted: Schema.Boolean, + }), + ), + is_finalized: Schema.Boolean, + }), + }), + ), + shared_client_theme: Schema.optional( + Schema.Struct({ + colors: Schema.Array(Schema.String), + gradient_angle: Schema.Number, + base_mix: Schema.Number, + base_theme: Schema.Unknown, + }), + ), + interaction_metadata: Schema.optional(Schema.Unknown), + message_snapshots: Schema.optional( + Schema.Array( + Schema.Struct({ + message: Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + }), + }), + ), + ), + reactions: Schema.optional( + Schema.Array( + Schema.Struct({ + emoji: Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + count: Schema.Number, + count_details: Schema.Struct({ + burst: Schema.Number, + normal: Schema.Number, + }), + burst_colors: Schema.Array(Schema.String), + me_burst: Schema.Boolean, + me: Schema.Boolean, + }), + ), + ), + referenced_message: Schema.optional(Schema.Unknown), +}); +export type GetMessageOutput = typeof GetMessageOutput.Type; + +// The operation +export const getMessage = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetMessageInput, + outputSchema: GetMessageOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getMyApplication.ts b/packages/discord/src/operations/getMyApplication.ts new file mode 100644 index 000000000..58353936d --- /dev/null +++ b/packages/discord/src/operations/getMyApplication.ts @@ -0,0 +1,108 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetMyApplicationInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + {}, +).pipe(T.Http({ method: "GET", path: "/applications/@me" })); +export type GetMyApplicationInput = typeof GetMyApplicationInput.Type; + +// Output Schema +export const GetMyApplicationOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + redirect_uris: Schema.Array(Schema.String), + interactions_endpoint_url: Schema.NullOr(Schema.String), + role_connections_verification_url: Schema.NullOr(Schema.String), + owner: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + approximate_guild_count: Schema.Number, + approximate_user_install_count: Schema.Number, + approximate_user_authorization_count: Schema.Number, + event_webhooks_url: Schema.optional(Schema.NullOr(Schema.String)), + event_webhooks_status: Schema.optional(Schema.Unknown), + event_webhooks_types: Schema.optional(Schema.Array(Schema.Unknown)), + explicit_content_filter: Schema.Unknown, + team: Schema.Unknown, + }, +); +export type GetMyApplicationOutput = typeof GetMyApplicationOutput.Type; + +// The operation +export const getMyApplication = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetMyApplicationInput, + outputSchema: GetMyApplicationOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getMyGuildMember.ts b/packages/discord/src/operations/getMyGuildMember.ts new file mode 100644 index 000000000..01ec898f5 --- /dev/null +++ b/packages/discord/src/operations/getMyGuildMember.ts @@ -0,0 +1,54 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetMyGuildMemberInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/users/@me/guilds/{guild_id}/member" })); +export type GetMyGuildMemberInput = typeof GetMyGuildMemberInput.Type; + +// Output Schema +export const GetMyGuildMemberOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + permissions: Schema.optional(Schema.String), + }, +); +export type GetMyGuildMemberOutput = typeof GetMyGuildMemberOutput.Type; + +// The operation +export const getMyGuildMember = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetMyGuildMemberInput, + outputSchema: GetMyGuildMemberOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getMyOauth2Application.ts b/packages/discord/src/operations/getMyOauth2Application.ts new file mode 100644 index 000000000..0cf3dfef0 --- /dev/null +++ b/packages/discord/src/operations/getMyOauth2Application.ts @@ -0,0 +1,112 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetMyOauth2ApplicationInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({}).pipe( + T.Http({ method: "GET", path: "/oauth2/applications/@me" }), + ); +export type GetMyOauth2ApplicationInput = + typeof GetMyOauth2ApplicationInput.Type; + +// Output Schema +export const GetMyOauth2ApplicationOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + redirect_uris: Schema.Array(Schema.String), + interactions_endpoint_url: Schema.NullOr(Schema.String), + role_connections_verification_url: Schema.NullOr(Schema.String), + owner: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + approximate_guild_count: Schema.Number, + approximate_user_install_count: Schema.Number, + approximate_user_authorization_count: Schema.Number, + event_webhooks_url: Schema.optional(Schema.NullOr(Schema.String)), + event_webhooks_status: Schema.optional(Schema.Unknown), + event_webhooks_types: Schema.optional(Schema.Array(Schema.Unknown)), + explicit_content_filter: Schema.Unknown, + team: Schema.Unknown, + }); +export type GetMyOauth2ApplicationOutput = + typeof GetMyOauth2ApplicationOutput.Type; + +// The operation +export const getMyOauth2Application = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: GetMyOauth2ApplicationInput, + outputSchema: GetMyOauth2ApplicationOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/getMyOauth2Authorization.ts b/packages/discord/src/operations/getMyOauth2Authorization.ts new file mode 100644 index 000000000..abb823161 --- /dev/null +++ b/packages/discord/src/operations/getMyOauth2Authorization.ts @@ -0,0 +1,107 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetMyOauth2AuthorizationInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({}).pipe( + T.Http({ method: "GET", path: "/oauth2/@me" }), + ); +export type GetMyOauth2AuthorizationInput = + typeof GetMyOauth2AuthorizationInput.Type; + +// Output Schema +export const GetMyOauth2AuthorizationOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application: Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + expires: Schema.String, + scopes: Schema.Array(Schema.Unknown), + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }); +export type GetMyOauth2AuthorizationOutput = + typeof GetMyOauth2AuthorizationOutput.Type; + +// The operation +export const getMyOauth2Authorization = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: GetMyOauth2AuthorizationInput, + outputSchema: GetMyOauth2AuthorizationOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/getMyUser.ts b/packages/discord/src/operations/getMyUser.ts new file mode 100644 index 000000000..25411406b --- /dev/null +++ b/packages/discord/src/operations/getMyUser.ts @@ -0,0 +1,41 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetMyUserInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + {}, +).pipe(T.Http({ method: "GET", path: "/users/@me" })); +export type GetMyUserInput = typeof GetMyUserInput.Type; + +// Output Schema +export const GetMyUserOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.optional(Schema.Unknown), + mfa_enabled: Schema.Boolean, + locale: Schema.Unknown, + premium_type: Schema.optional(Schema.Unknown), + email: Schema.optional(Schema.NullOr(Schema.String)), + verified: Schema.optional(Schema.Boolean), +}); +export type GetMyUserOutput = typeof GetMyUserOutput.Type; + +// The operation +export const getMyUser = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetMyUserInput, + outputSchema: GetMyUserOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getOpenidConnectUserinfo.ts b/packages/discord/src/operations/getOpenidConnectUserinfo.ts new file mode 100644 index 000000000..877e012d4 --- /dev/null +++ b/packages/discord/src/operations/getOpenidConnectUserinfo.ts @@ -0,0 +1,35 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetOpenidConnectUserinfoInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({}).pipe( + T.Http({ method: "GET", path: "/oauth2/userinfo" }), + ); +export type GetOpenidConnectUserinfoInput = + typeof GetOpenidConnectUserinfoInput.Type; + +// Output Schema +export const GetOpenidConnectUserinfoOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + sub: Schema.String, + email: Schema.optional(Schema.NullOr(Schema.String)), + email_verified: Schema.optional(Schema.Boolean), + preferred_username: Schema.optional(Schema.String), + nickname: Schema.optional(Schema.NullOr(Schema.String)), + picture: Schema.optional(Schema.String), + locale: Schema.optional(Schema.String), + }); +export type GetOpenidConnectUserinfoOutput = + typeof GetOpenidConnectUserinfoOutput.Type; + +// The operation +export const getOpenidConnectUserinfo = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: GetOpenidConnectUserinfoInput, + outputSchema: GetOpenidConnectUserinfoOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/getOriginalWebhookMessage.ts b/packages/discord/src/operations/getOriginalWebhookMessage.ts new file mode 100644 index 000000000..a3867d773 --- /dev/null +++ b/packages/discord/src/operations/getOriginalWebhookMessage.ts @@ -0,0 +1,863 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetOriginalWebhookMessageInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + webhook_id: Schema.String.pipe(T.PathParam()), + webhook_token: Schema.String.pipe(T.PathParam()), + thread_id: Schema.optional(Schema.String), + }).pipe( + T.Http({ + method: "GET", + path: "/webhooks/{webhook_id}/{webhook_token}/messages/@original", + }), + ); +export type GetOriginalWebhookMessageInput = + typeof GetOriginalWebhookMessageInput.Type; + +// Output Schema +export const GetOriginalWebhookMessageOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + id: Schema.String, + channel_id: Schema.String, + author: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + pinned: Schema.Boolean, + mention_everyone: Schema.Boolean, + tts: Schema.Boolean, + call: Schema.optional( + Schema.Struct({ + ended_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + participants: Schema.Array(Schema.String), + }), + ), + activity: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + party_id: Schema.optional(Schema.String), + }), + ), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + application_id: Schema.optional(Schema.String), + interaction: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + name_localized: Schema.optional(Schema.String), + }), + ), + nonce: Schema.optional(Schema.Unknown), + webhook_id: Schema.optional(Schema.String), + message_reference: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + channel_id: Schema.String, + message_id: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + }), + ), + thread: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + mention_channels: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + type: Schema.Unknown, + guild_id: Schema.String, + }), + ), + ), + role_subscription_data: Schema.optional( + Schema.Struct({ + role_subscription_listing_id: Schema.String, + tier_name: Schema.String, + total_months_subscribed: Schema.Number, + is_renewal: Schema.Boolean, + }), + ), + purchase_notification: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + guild_product_purchase: Schema.optional( + Schema.Struct({ + listing_id: Schema.String, + product_name: Schema.String, + }), + ), + }), + ), + position: Schema.optional(Schema.Number), + resolved: Schema.optional( + Schema.Struct({ + users: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + ), + members: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + channels: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + ), + roles: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ), + ), + ), + }), + ), + poll: Schema.optional( + Schema.Struct({ + question: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + answers: Schema.Array( + Schema.Struct({ + answer_id: Schema.Number, + poll_media: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + }), + ), + expiry: Schema.String, + allow_multiselect: Schema.Boolean, + layout_type: Schema.Unknown, + results: Schema.Struct({ + answer_counts: Schema.Array( + Schema.Struct({ + id: Schema.Number, + count: Schema.Number, + me_voted: Schema.Boolean, + }), + ), + is_finalized: Schema.Boolean, + }), + }), + ), + shared_client_theme: Schema.optional( + Schema.Struct({ + colors: Schema.Array(Schema.String), + gradient_angle: Schema.Number, + base_mix: Schema.Number, + base_theme: Schema.Unknown, + }), + ), + interaction_metadata: Schema.optional(Schema.Unknown), + message_snapshots: Schema.optional( + Schema.Array( + Schema.Struct({ + message: Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + }), + }), + ), + ), + reactions: Schema.optional( + Schema.Array( + Schema.Struct({ + emoji: Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + count: Schema.Number, + count_details: Schema.Struct({ + burst: Schema.Number, + normal: Schema.Number, + }), + burst_colors: Schema.Array(Schema.String), + me_burst: Schema.Boolean, + me: Schema.Boolean, + }), + ), + ), + referenced_message: Schema.optional(Schema.Unknown), + }); +export type GetOriginalWebhookMessageOutput = + typeof GetOriginalWebhookMessageOutput.Type; + +// The operation +export const getOriginalWebhookMessage = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: GetOriginalWebhookMessageInput, + outputSchema: GetOriginalWebhookMessageOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/getPublicKeys.ts b/packages/discord/src/operations/getPublicKeys.ts new file mode 100644 index 000000000..b0d58385e --- /dev/null +++ b/packages/discord/src/operations/getPublicKeys.ts @@ -0,0 +1,32 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetPublicKeysInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + {}, +).pipe(T.Http({ method: "GET", path: "/oauth2/keys" })); +export type GetPublicKeysInput = typeof GetPublicKeysInput.Type; + +// Output Schema +export const GetPublicKeysOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + keys: Schema.Array( + Schema.Struct({ + kty: Schema.String, + use: Schema.String, + kid: Schema.String, + n: Schema.String, + e: Schema.String, + alg: Schema.String, + }), + ), +}); +export type GetPublicKeysOutput = typeof GetPublicKeysOutput.Type; + +// The operation +export const getPublicKeys = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetPublicKeysInput, + outputSchema: GetPublicKeysOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getSelfVoiceState.ts b/packages/discord/src/operations/getSelfVoiceState.ts new file mode 100644 index 000000000..422a02e56 --- /dev/null +++ b/packages/discord/src/operations/getSelfVoiceState.ts @@ -0,0 +1,70 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetSelfVoiceStateInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + guild_id: Schema.String.pipe(T.PathParam()), + }, +).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/voice-states/@me" })); +export type GetSelfVoiceStateInput = typeof GetSelfVoiceStateInput.Type; + +// Output Schema +export const GetSelfVoiceStateOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.Unknown, + deaf: Schema.Boolean, + guild_id: Schema.Unknown, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + mute: Schema.Boolean, + request_to_speak_timestamp: Schema.NullOr(Schema.String), + suppress: Schema.Boolean, + self_stream: Schema.NullOr(Schema.Boolean), + self_deaf: Schema.Boolean, + self_mute: Schema.Boolean, + self_video: Schema.Boolean, + session_id: Schema.String, + user_id: Schema.String, + }); +export type GetSelfVoiceStateOutput = typeof GetSelfVoiceStateOutput.Type; + +// The operation +export const getSelfVoiceState = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetSelfVoiceStateInput, + outputSchema: GetSelfVoiceStateOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getSoundboardDefaultSounds.ts b/packages/discord/src/operations/getSoundboardDefaultSounds.ts new file mode 100644 index 000000000..3507cd76f --- /dev/null +++ b/packages/discord/src/operations/getSoundboardDefaultSounds.ts @@ -0,0 +1,55 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetSoundboardDefaultSoundsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({}).pipe( + T.Http({ method: "GET", path: "/soundboard-default-sounds" }), + ); +export type GetSoundboardDefaultSoundsInput = + typeof GetSoundboardDefaultSoundsInput.Type; + +// Output Schema +export const GetSoundboardDefaultSoundsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + name: Schema.String, + sound_id: Schema.String, + volume: Schema.Number, + emoji_id: Schema.Unknown, + emoji_name: Schema.NullOr(Schema.String), + guild_id: Schema.optional(Schema.String), + available: Schema.Boolean, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ); +export type GetSoundboardDefaultSoundsOutput = + typeof GetSoundboardDefaultSoundsOutput.Type; + +// The operation +export const getSoundboardDefaultSounds = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: GetSoundboardDefaultSoundsInput, + outputSchema: GetSoundboardDefaultSoundsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/getStageInstance.ts b/packages/discord/src/operations/getStageInstance.ts new file mode 100644 index 000000000..5313ad20b --- /dev/null +++ b/packages/discord/src/operations/getStageInstance.ts @@ -0,0 +1,31 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetStageInstanceInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/stage-instances/{channel_id}" })); +export type GetStageInstanceInput = typeof GetStageInstanceInput.Type; + +// Output Schema +export const GetStageInstanceOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + guild_id: Schema.String, + channel_id: Schema.String, + topic: Schema.String, + privacy_level: Schema.Unknown, + id: Schema.String, + discoverable_disabled: Schema.Boolean, + guild_scheduled_event_id: Schema.Unknown, + }, +); +export type GetStageInstanceOutput = typeof GetStageInstanceOutput.Type; + +// The operation +export const getStageInstance = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetStageInstanceInput, + outputSchema: GetStageInstanceOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getSticker.ts b/packages/discord/src/operations/getSticker.ts new file mode 100644 index 000000000..949f6d608 --- /dev/null +++ b/packages/discord/src/operations/getSticker.ts @@ -0,0 +1,21 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetStickerInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + sticker_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/stickers/{sticker_id}" })); +export type GetStickerInput = typeof GetStickerInput.Type; + +// Output Schema +export const GetStickerOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Unknown; +export type GetStickerOutput = typeof GetStickerOutput.Type; + +// The operation +export const getSticker = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetStickerInput, + outputSchema: GetStickerOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getStickerPack.ts b/packages/discord/src/operations/getStickerPack.ts new file mode 100644 index 000000000..ed1622d88 --- /dev/null +++ b/packages/discord/src/operations/getStickerPack.ts @@ -0,0 +1,40 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetStickerPackInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + pack_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/sticker-packs/{pack_id}" })); +export type GetStickerPackInput = typeof GetStickerPackInput.Type; + +// Output Schema +export const GetStickerPackOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + sku_id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + stickers: Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + tags: Schema.String, + type: Schema.Unknown, + format_type: Schema.Unknown, + description: Schema.NullOr(Schema.String), + pack_id: Schema.String, + sort_value: Schema.Number, + }), + ), + cover_sticker_id: Schema.optional(Schema.String), + banner_asset_id: Schema.optional(Schema.String), +}); +export type GetStickerPackOutput = typeof GetStickerPackOutput.Type; + +// The operation +export const getStickerPack = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetStickerPackInput, + outputSchema: GetStickerPackOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getThreadMember.ts b/packages/discord/src/operations/getThreadMember.ts new file mode 100644 index 000000000..927be165f --- /dev/null +++ b/packages/discord/src/operations/getThreadMember.ts @@ -0,0 +1,66 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetThreadMemberInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + user_id: Schema.String.pipe(T.PathParam()), + with_member: Schema.optional(Schema.Boolean), +}).pipe( + T.Http({ + method: "GET", + path: "/channels/{channel_id}/thread-members/{user_id}", + }), +); +export type GetThreadMemberInput = typeof GetThreadMemberInput.Type; + +// Output Schema +export const GetThreadMemberOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), +}); +export type GetThreadMemberOutput = typeof GetThreadMemberOutput.Type; + +// The operation +export const getThreadMember = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetThreadMemberInput, + outputSchema: GetThreadMemberOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getUser.ts b/packages/discord/src/operations/getUser.ts new file mode 100644 index 000000000..7ee94abe7 --- /dev/null +++ b/packages/discord/src/operations/getUser.ts @@ -0,0 +1,36 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetUserInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + user_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/users/{user_id}" })); +export type GetUserInput = typeof GetUserInput.Type; + +// Output Schema +export const GetUserOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, +}); +export type GetUserOutput = typeof GetUserOutput.Type; + +// The operation +export const getUser = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetUserInput, + outputSchema: GetUserOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getVoiceState.ts b/packages/discord/src/operations/getVoiceState.ts new file mode 100644 index 000000000..ed090633d --- /dev/null +++ b/packages/discord/src/operations/getVoiceState.ts @@ -0,0 +1,70 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetVoiceStateInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + user_id: Schema.String.pipe(T.PathParam()), +}).pipe( + T.Http({ method: "GET", path: "/guilds/{guild_id}/voice-states/{user_id}" }), +); +export type GetVoiceStateInput = typeof GetVoiceStateInput.Type; + +// Output Schema +export const GetVoiceStateOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.Unknown, + deaf: Schema.Boolean, + guild_id: Schema.Unknown, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + mute: Schema.Boolean, + request_to_speak_timestamp: Schema.NullOr(Schema.String), + suppress: Schema.Boolean, + self_stream: Schema.NullOr(Schema.Boolean), + self_deaf: Schema.Boolean, + self_mute: Schema.Boolean, + self_video: Schema.Boolean, + session_id: Schema.String, + user_id: Schema.String, +}); +export type GetVoiceStateOutput = typeof GetVoiceStateOutput.Type; + +// The operation +export const getVoiceState = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetVoiceStateInput, + outputSchema: GetVoiceStateOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getWebhook.ts b/packages/discord/src/operations/getWebhook.ts new file mode 100644 index 000000000..ed2afa5c5 --- /dev/null +++ b/packages/discord/src/operations/getWebhook.ts @@ -0,0 +1,21 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetWebhookInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + webhook_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/webhooks/{webhook_id}" })); +export type GetWebhookInput = typeof GetWebhookInput.Type; + +// Output Schema +export const GetWebhookOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Unknown; +export type GetWebhookOutput = typeof GetWebhookOutput.Type; + +// The operation +export const getWebhook = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetWebhookInput, + outputSchema: GetWebhookOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getWebhookByToken.ts b/packages/discord/src/operations/getWebhookByToken.ts new file mode 100644 index 000000000..c3fda1acf --- /dev/null +++ b/packages/discord/src/operations/getWebhookByToken.ts @@ -0,0 +1,27 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetWebhookByTokenInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + webhook_id: Schema.String.pipe(T.PathParam()), + webhook_token: Schema.String.pipe(T.PathParam()), + }, +).pipe( + T.Http({ method: "GET", path: "/webhooks/{webhook_id}/{webhook_token}" }), +); +export type GetWebhookByTokenInput = typeof GetWebhookByTokenInput.Type; + +// Output Schema +export const GetWebhookByTokenOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Unknown; +export type GetWebhookByTokenOutput = typeof GetWebhookByTokenOutput.Type; + +// The operation +export const getWebhookByToken = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetWebhookByTokenInput, + outputSchema: GetWebhookByTokenOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/getWebhookMessage.ts b/packages/discord/src/operations/getWebhookMessage.ts new file mode 100644 index 000000000..0bb6d8fe8 --- /dev/null +++ b/packages/discord/src/operations/getWebhookMessage.ts @@ -0,0 +1,861 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetWebhookMessageInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + webhook_id: Schema.String.pipe(T.PathParam()), + webhook_token: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), + thread_id: Schema.optional(Schema.String), + }, +).pipe( + T.Http({ + method: "GET", + path: "/webhooks/{webhook_id}/{webhook_token}/messages/{message_id}", + }), +); +export type GetWebhookMessageInput = typeof GetWebhookMessageInput.Type; + +// Output Schema +export const GetWebhookMessageOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + id: Schema.String, + channel_id: Schema.String, + author: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + pinned: Schema.Boolean, + mention_everyone: Schema.Boolean, + tts: Schema.Boolean, + call: Schema.optional( + Schema.Struct({ + ended_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + participants: Schema.Array(Schema.String), + }), + ), + activity: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + party_id: Schema.optional(Schema.String), + }), + ), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + application_id: Schema.optional(Schema.String), + interaction: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + name_localized: Schema.optional(Schema.String), + }), + ), + nonce: Schema.optional(Schema.Unknown), + webhook_id: Schema.optional(Schema.String), + message_reference: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + channel_id: Schema.String, + message_id: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + }), + ), + thread: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + mention_channels: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + type: Schema.Unknown, + guild_id: Schema.String, + }), + ), + ), + role_subscription_data: Schema.optional( + Schema.Struct({ + role_subscription_listing_id: Schema.String, + tier_name: Schema.String, + total_months_subscribed: Schema.Number, + is_renewal: Schema.Boolean, + }), + ), + purchase_notification: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + guild_product_purchase: Schema.optional( + Schema.Struct({ + listing_id: Schema.String, + product_name: Schema.String, + }), + ), + }), + ), + position: Schema.optional(Schema.Number), + resolved: Schema.optional( + Schema.Struct({ + users: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + ), + members: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + channels: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + ), + roles: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ), + ), + ), + }), + ), + poll: Schema.optional( + Schema.Struct({ + question: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + answers: Schema.Array( + Schema.Struct({ + answer_id: Schema.Number, + poll_media: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + }), + ), + expiry: Schema.String, + allow_multiselect: Schema.Boolean, + layout_type: Schema.Unknown, + results: Schema.Struct({ + answer_counts: Schema.Array( + Schema.Struct({ + id: Schema.Number, + count: Schema.Number, + me_voted: Schema.Boolean, + }), + ), + is_finalized: Schema.Boolean, + }), + }), + ), + shared_client_theme: Schema.optional( + Schema.Struct({ + colors: Schema.Array(Schema.String), + gradient_angle: Schema.Number, + base_mix: Schema.Number, + base_theme: Schema.Unknown, + }), + ), + interaction_metadata: Schema.optional(Schema.Unknown), + message_snapshots: Schema.optional( + Schema.Array( + Schema.Struct({ + message: Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + }), + }), + ), + ), + reactions: Schema.optional( + Schema.Array( + Schema.Struct({ + emoji: Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + count: Schema.Number, + count_details: Schema.Struct({ + burst: Schema.Number, + normal: Schema.Number, + }), + burst_colors: Schema.Array(Schema.String), + me_burst: Schema.Boolean, + me: Schema.Boolean, + }), + ), + ), + referenced_message: Schema.optional(Schema.Unknown), + }); +export type GetWebhookMessageOutput = typeof GetWebhookMessageOutput.Type; + +// The operation +export const getWebhookMessage = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetWebhookMessageInput, + outputSchema: GetWebhookMessageOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/guildRoleMemberCounts.ts b/packages/discord/src/operations/guildRoleMemberCounts.ts new file mode 100644 index 000000000..94964e645 --- /dev/null +++ b/packages/discord/src/operations/guildRoleMemberCounts.ts @@ -0,0 +1,28 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GuildRoleMemberCountsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ method: "GET", path: "/guilds/{guild_id}/roles/member-counts" }), + ); +export type GuildRoleMemberCountsInput = typeof GuildRoleMemberCountsInput.Type; + +// Output Schema +export const GuildRoleMemberCountsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Record(Schema.String, Schema.Number); +export type GuildRoleMemberCountsOutput = + typeof GuildRoleMemberCountsOutput.Type; + +// The operation +export const guildRoleMemberCounts = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: GuildRoleMemberCountsInput, + outputSchema: GuildRoleMemberCountsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/guildSearch.ts b/packages/discord/src/operations/guildSearch.ts new file mode 100644 index 000000000..ee2fb2d91 --- /dev/null +++ b/packages/discord/src/operations/guildSearch.ts @@ -0,0 +1,1015 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GuildSearchInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + sort_by: Schema.optional(Schema.String), + sort_order: Schema.optional(Schema.String), + content: Schema.optional(Schema.String), + slop: Schema.optional(Schema.Number), + author_id: Schema.optional(Schema.String), + author_type: Schema.optional(Schema.String), + mentions: Schema.optional(Schema.String), + mentions_role_id: Schema.optional(Schema.String), + replied_to_user_id: Schema.optional(Schema.String), + replied_to_message_id: Schema.optional(Schema.String), + mention_everyone: Schema.optional(Schema.Boolean), + min_id: Schema.optional(Schema.String), + max_id: Schema.optional(Schema.String), + limit: Schema.optional(Schema.Number), + offset: Schema.optional(Schema.Number), + has: Schema.optional(Schema.String), + link_hostname: Schema.optional(Schema.String), + embed_provider: Schema.optional(Schema.String), + embed_type: Schema.optional(Schema.String), + attachment_extension: Schema.optional(Schema.String), + attachment_filename: Schema.optional(Schema.String), + pinned: Schema.optional(Schema.Boolean), + include_nsfw: Schema.optional(Schema.Boolean), + channel_id: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/messages/search" })); +export type GuildSearchInput = typeof GuildSearchInput.Type; + +// Output Schema +export const GuildSearchOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + messages: Schema.Array( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + id: Schema.String, + channel_id: Schema.String, + author: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + pinned: Schema.Boolean, + mention_everyone: Schema.Boolean, + tts: Schema.Boolean, + call: Schema.optional( + Schema.Struct({ + ended_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + participants: Schema.Array(Schema.String), + }), + ), + activity: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + party_id: Schema.optional(Schema.String), + }), + ), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + application_id: Schema.optional(Schema.String), + interaction: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + name_localized: Schema.optional(Schema.String), + }), + ), + nonce: Schema.optional(Schema.Unknown), + webhook_id: Schema.optional(Schema.String), + message_reference: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + channel_id: Schema.String, + message_id: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + }), + ), + thread: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + mention_channels: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + type: Schema.Unknown, + guild_id: Schema.String, + }), + ), + ), + role_subscription_data: Schema.optional( + Schema.Struct({ + role_subscription_listing_id: Schema.String, + tier_name: Schema.String, + total_months_subscribed: Schema.Number, + is_renewal: Schema.Boolean, + }), + ), + purchase_notification: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + guild_product_purchase: Schema.optional( + Schema.Struct({ + listing_id: Schema.String, + product_name: Schema.String, + }), + ), + }), + ), + position: Schema.optional(Schema.Number), + resolved: Schema.optional( + Schema.Struct({ + users: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + ), + members: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + channels: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + ), + roles: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ), + ), + ), + }), + ), + poll: Schema.optional( + Schema.Struct({ + question: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + answers: Schema.Array( + Schema.Struct({ + answer_id: Schema.Number, + poll_media: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + }), + ), + expiry: Schema.String, + allow_multiselect: Schema.Boolean, + layout_type: Schema.Unknown, + results: Schema.Struct({ + answer_counts: Schema.Array( + Schema.Struct({ + id: Schema.Number, + count: Schema.Number, + me_voted: Schema.Boolean, + }), + ), + is_finalized: Schema.Boolean, + }), + }), + ), + shared_client_theme: Schema.optional( + Schema.Struct({ + colors: Schema.Array(Schema.String), + gradient_angle: Schema.Number, + base_mix: Schema.Number, + base_theme: Schema.Unknown, + }), + ), + interaction_metadata: Schema.optional(Schema.Unknown), + message_snapshots: Schema.optional( + Schema.Array( + Schema.Struct({ + message: Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional( + Schema.NullOr(Schema.String), + ), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional( + Schema.Unknown, + ), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional( + Schema.Array(Schema.String), + ), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional( + Schema.Unknown, + ), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + }), + }), + ), + ), + reactions: Schema.optional( + Schema.Array( + Schema.Struct({ + emoji: Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + count: Schema.Number, + count_details: Schema.Struct({ + burst: Schema.Number, + normal: Schema.Number, + }), + burst_colors: Schema.Array(Schema.String), + me_burst: Schema.Boolean, + me: Schema.Boolean, + }), + ), + ), + referenced_message: Schema.optional(Schema.Unknown), + hit: Schema.Boolean, + }), + ), + ), + doing_deep_historical_index: Schema.Boolean, + total_results: Schema.Number, + threads: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + ), + ), + members: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + ), + ), + documents_indexed: Schema.optional(Schema.NullOr(Schema.Number)), +}); +export type GuildSearchOutput = typeof GuildSearchOutput.Type; + +// The operation +export const guildSearch = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GuildSearchInput, + outputSchema: GuildSearchOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/index.ts b/packages/discord/src/operations/index.ts new file mode 100644 index 000000000..f80d74af1 --- /dev/null +++ b/packages/discord/src/operations/index.ts @@ -0,0 +1,233 @@ +export * from "./getMyApplication.ts"; +export * from "./updateMyApplication.ts"; +export * from "./getApplication.ts"; +export * from "./updateApplication.ts"; +export * from "./applicationsGetActivityInstance.ts"; +export * from "./uploadApplicationAttachment.ts"; +export * from "./listApplicationCommands.ts"; +export * from "./createApplicationCommand.ts"; +export * from "./bulkSetApplicationCommands.ts"; +export * from "./getApplicationCommand.ts"; +export * from "./updateApplicationCommand.ts"; +export * from "./deleteApplicationCommand.ts"; +export * from "./listApplicationEmojis.ts"; +export * from "./createApplicationEmoji.ts"; +export * from "./getApplicationEmoji.ts"; +export * from "./updateApplicationEmoji.ts"; +export * from "./deleteApplicationEmoji.ts"; +export * from "./getEntitlements.ts"; +export * from "./createEntitlement.ts"; +export * from "./getEntitlement.ts"; +export * from "./deleteEntitlement.ts"; +export * from "./consumeEntitlement.ts"; +export * from "./listGuildApplicationCommands.ts"; +export * from "./createGuildApplicationCommand.ts"; +export * from "./bulkSetGuildApplicationCommands.ts"; +export * from "./listGuildApplicationCommandPermissions.ts"; +export * from "./getGuildApplicationCommand.ts"; +export * from "./updateGuildApplicationCommand.ts"; +export * from "./deleteGuildApplicationCommand.ts"; +export * from "./getGuildApplicationCommandPermissions.ts"; +export * from "./setGuildApplicationCommandPermissions.ts"; +export * from "./getApplicationRoleConnectionsMetadata.ts"; +export * from "./updateApplicationRoleConnectionsMetadata.ts"; +export * from "./getChannel.ts"; +export * from "./updateChannel.ts"; +export * from "./deleteChannel.ts"; +export * from "./followChannel.ts"; +export * from "./listChannelInvites.ts"; +export * from "./createChannelInvite.ts"; +export * from "./listMessages.ts"; +export * from "./createMessage.ts"; +export * from "./bulkDeleteMessages.ts"; +export * from "./listPins.ts"; +export * from "./createPin.ts"; +export * from "./deletePin.ts"; +export * from "./getMessage.ts"; +export * from "./updateMessage.ts"; +export * from "./deleteMessage.ts"; +export * from "./crosspostMessage.ts"; +export * from "./deleteAllMessageReactions.ts"; +export * from "./listMessageReactionsByEmoji.ts"; +export * from "./deleteAllMessageReactionsByEmoji.ts"; +export * from "./addMyMessageReaction.ts"; +export * from "./deleteMyMessageReaction.ts"; +export * from "./deleteUserMessageReaction.ts"; +export * from "./createThreadFromMessage.ts"; +export * from "./setChannelPermissionOverwrite.ts"; +export * from "./deleteChannelPermissionOverwrite.ts"; +export * from "./deprecatedListPins.ts"; +export * from "./deprecatedCreatePin.ts"; +export * from "./deprecatedDeletePin.ts"; +export * from "./getAnswerVoters.ts"; +export * from "./pollExpire.ts"; +export * from "./addGroupDmUser.ts"; +export * from "./deleteGroupDmUser.ts"; +export * from "./sendSoundboardSound.ts"; +export * from "./listThreadMembers.ts"; +export * from "./joinThread.ts"; +export * from "./leaveThread.ts"; +export * from "./getThreadMember.ts"; +export * from "./addThreadMember.ts"; +export * from "./deleteThreadMember.ts"; +export * from "./createThread.ts"; +export * from "./listPrivateArchivedThreads.ts"; +export * from "./listPublicArchivedThreads.ts"; +export * from "./threadSearch.ts"; +export * from "./triggerTypingIndicator.ts"; +export * from "./listMyPrivateArchivedThreads.ts"; +export * from "./updateVoiceChannelStatus.ts"; +export * from "./listChannelWebhooks.ts"; +export * from "./createWebhook.ts"; +export * from "./getGateway.ts"; +export * from "./getBotGateway.ts"; +export * from "./getGuildTemplate.ts"; +export * from "./getGuild.ts"; +export * from "./updateGuild.ts"; +export * from "./listGuildAuditLogEntries.ts"; +export * from "./listAutoModerationRules.ts"; +export * from "./createAutoModerationRule.ts"; +export * from "./getAutoModerationRule.ts"; +export * from "./updateAutoModerationRule.ts"; +export * from "./deleteAutoModerationRule.ts"; +export * from "./listGuildBans.ts"; +export * from "./getGuildBan.ts"; +export * from "./banUserFromGuild.ts"; +export * from "./unbanUserFromGuild.ts"; +export * from "./bulkBanUsersFromGuild.ts"; +export * from "./listGuildChannels.ts"; +export * from "./createGuildChannel.ts"; +export * from "./bulkUpdateGuildChannels.ts"; +export * from "./listGuildEmojis.ts"; +export * from "./createGuildEmoji.ts"; +export * from "./getGuildEmoji.ts"; +export * from "./updateGuildEmoji.ts"; +export * from "./deleteGuildEmoji.ts"; +export * from "./listGuildIntegrations.ts"; +export * from "./deleteGuildIntegration.ts"; +export * from "./listGuildInvites.ts"; +export * from "./listGuildMembers.ts"; +export * from "./updateMyGuildMember.ts"; +export * from "./searchGuildMembers.ts"; +export * from "./getGuildMember.ts"; +export * from "./addGuildMember.ts"; +export * from "./updateGuildMember.ts"; +export * from "./deleteGuildMember.ts"; +export * from "./addGuildMemberRole.ts"; +export * from "./deleteGuildMemberRole.ts"; +export * from "./guildSearch.ts"; +export * from "./getGuildNewMemberWelcome.ts"; +export * from "./getGuildsOnboarding.ts"; +export * from "./putGuildsOnboarding.ts"; +export * from "./getGuildPreview.ts"; +export * from "./previewPruneGuild.ts"; +export * from "./pruneGuild.ts"; +export * from "./listGuildVoiceRegions.ts"; +export * from "./getGuildJoinRequests.ts"; +export * from "./actionGuildJoinRequest.ts"; +export * from "./listGuildRoles.ts"; +export * from "./createGuildRole.ts"; +export * from "./bulkUpdateGuildRoles.ts"; +export * from "./guildRoleMemberCounts.ts"; +export * from "./getGuildRole.ts"; +export * from "./updateGuildRole.ts"; +export * from "./deleteGuildRole.ts"; +export * from "./listGuildScheduledEvents.ts"; +export * from "./createGuildScheduledEvent.ts"; +export * from "./getGuildScheduledEvent.ts"; +export * from "./updateGuildScheduledEvent.ts"; +export * from "./deleteGuildScheduledEvent.ts"; +export * from "./listGuildScheduledEventUsers.ts"; +export * from "./listGuildSoundboardSounds.ts"; +export * from "./createGuildSoundboardSound.ts"; +export * from "./getGuildSoundboardSound.ts"; +export * from "./updateGuildSoundboardSound.ts"; +export * from "./deleteGuildSoundboardSound.ts"; +export * from "./listGuildStickers.ts"; +export * from "./createGuildSticker.ts"; +export * from "./getGuildSticker.ts"; +export * from "./updateGuildSticker.ts"; +export * from "./deleteGuildSticker.ts"; +export * from "./listGuildTemplates.ts"; +export * from "./createGuildTemplate.ts"; +export * from "./syncGuildTemplate.ts"; +export * from "./updateGuildTemplate.ts"; +export * from "./deleteGuildTemplate.ts"; +export * from "./getActiveGuildThreads.ts"; +export * from "./getGuildVanityUrl.ts"; +export * from "./getSelfVoiceState.ts"; +export * from "./updateSelfVoiceState.ts"; +export * from "./getVoiceState.ts"; +export * from "./updateVoiceState.ts"; +export * from "./getGuildWebhooks.ts"; +export * from "./getGuildWelcomeScreen.ts"; +export * from "./updateGuildWelcomeScreen.ts"; +export * from "./getGuildWidgetSettings.ts"; +export * from "./updateGuildWidgetSettings.ts"; +export * from "./getGuildWidget.ts"; +export * from "./getGuildWidgetPng.ts"; +export * from "./createInteractionResponse.ts"; +export * from "./inviteResolve.ts"; +export * from "./inviteRevoke.ts"; +export * from "./getInviteTargetUsers.ts"; +export * from "./updateInviteTargetUsers.ts"; +export * from "./getInviteTargetUsersJobStatus.ts"; +export * from "./createLobby.ts"; +export * from "./createOrJoinLobby.ts"; +export * from "./getLobby.ts"; +export * from "./editLobby.ts"; +export * from "./editLobbyChannelLink.ts"; +export * from "./leaveLobby.ts"; +export * from "./createLinkedLobbyGuildInviteForSelf.ts"; +export * from "./bulkUpdateLobbyMembers.ts"; +export * from "./addLobbyMember.ts"; +export * from "./deleteLobbyMember.ts"; +export * from "./createLinkedLobbyGuildInviteForUser.ts"; +export * from "./getLobbyMessages.ts"; +export * from "./createLobbyMessage.ts"; +export * from "./updateLobbyMessageExternalModerationMetadata.ts"; +export * from "./getMyOauth2Authorization.ts"; +export * from "./getMyOauth2Application.ts"; +export * from "./getPublicKeys.ts"; +export * from "./getOpenidConnectUserinfo.ts"; +export * from "./updateUserMessageExternalModerationMetadata.ts"; +export * from "./partnerSdkUnmergeProvisionalAccount.ts"; +export * from "./botPartnerSdkUnmergeProvisionalAccount.ts"; +export * from "./partnerSdkToken.ts"; +export * from "./botPartnerSdkToken.ts"; +export * from "./getSoundboardDefaultSounds.ts"; +export * from "./createStageInstance.ts"; +export * from "./getStageInstance.ts"; +export * from "./updateStageInstance.ts"; +export * from "./deleteStageInstance.ts"; +export * from "./listStickerPacks.ts"; +export * from "./getStickerPack.ts"; +export * from "./getSticker.ts"; +export * from "./getMyUser.ts"; +export * from "./updateMyUser.ts"; +export * from "./getCurrentUserApplicationEntitlements.ts"; +export * from "./getApplicationUserRoleConnection.ts"; +export * from "./updateApplicationUserRoleConnection.ts"; +export * from "./deleteApplicationUserRoleConnection.ts"; +export * from "./createDm.ts"; +export * from "./listMyConnections.ts"; +export * from "./listMyGuilds.ts"; +export * from "./leaveGuild.ts"; +export * from "./getMyGuildMember.ts"; +export * from "./getUser.ts"; +export * from "./listVoiceRegions.ts"; +export * from "./getWebhook.ts"; +export * from "./updateWebhook.ts"; +export * from "./deleteWebhook.ts"; +export * from "./getWebhookByToken.ts"; +export * from "./executeWebhook.ts"; +export * from "./updateWebhookByToken.ts"; +export * from "./deleteWebhookByToken.ts"; +export * from "./executeGithubCompatibleWebhook.ts"; +export * from "./getOriginalWebhookMessage.ts"; +export * from "./updateOriginalWebhookMessage.ts"; +export * from "./deleteOriginalWebhookMessage.ts"; +export * from "./getWebhookMessage.ts"; +export * from "./updateWebhookMessage.ts"; +export * from "./deleteWebhookMessage.ts"; +export * from "./executeSlackCompatibleWebhook.ts"; diff --git a/packages/discord/src/operations/inviteResolve.ts b/packages/discord/src/operations/inviteResolve.ts new file mode 100644 index 000000000..0f9565980 --- /dev/null +++ b/packages/discord/src/operations/inviteResolve.ts @@ -0,0 +1,23 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const InviteResolveInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + code: Schema.String.pipe(T.PathParam()), + with_counts: Schema.optional(Schema.Boolean), + guild_scheduled_event_id: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "GET", path: "/invites/{code}" })); +export type InviteResolveInput = typeof InviteResolveInput.Type; + +// Output Schema +export const InviteResolveOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Unknown; +export type InviteResolveOutput = typeof InviteResolveOutput.Type; + +// The operation +export const inviteResolve = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: InviteResolveInput, + outputSchema: InviteResolveOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/inviteRevoke.ts b/packages/discord/src/operations/inviteRevoke.ts new file mode 100644 index 000000000..7e1fc0b9a --- /dev/null +++ b/packages/discord/src/operations/inviteRevoke.ts @@ -0,0 +1,21 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const InviteRevokeInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + code: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "DELETE", path: "/invites/{code}" })); +export type InviteRevokeInput = typeof InviteRevokeInput.Type; + +// Output Schema +export const InviteRevokeOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Unknown; +export type InviteRevokeOutput = typeof InviteRevokeOutput.Type; + +// The operation +export const inviteRevoke = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: InviteRevokeInput, + outputSchema: InviteRevokeOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/joinThread.ts b/packages/discord/src/operations/joinThread.ts new file mode 100644 index 000000000..166aa3dff --- /dev/null +++ b/packages/discord/src/operations/joinThread.ts @@ -0,0 +1,23 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const JoinThreadInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), +}).pipe( + T.Http({ method: "PUT", path: "/channels/{channel_id}/thread-members/@me" }), +); +export type JoinThreadInput = typeof JoinThreadInput.Type; + +// Output Schema +export const JoinThreadOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type JoinThreadOutput = typeof JoinThreadOutput.Type; + +// The operation +export const joinThread = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: JoinThreadInput, + outputSchema: JoinThreadOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/leaveGuild.ts b/packages/discord/src/operations/leaveGuild.ts new file mode 100644 index 000000000..3959c7128 --- /dev/null +++ b/packages/discord/src/operations/leaveGuild.ts @@ -0,0 +1,21 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const LeaveGuildInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "DELETE", path: "/users/@me/guilds/{guild_id}" })); +export type LeaveGuildInput = typeof LeaveGuildInput.Type; + +// Output Schema +export const LeaveGuildOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type LeaveGuildOutput = typeof LeaveGuildOutput.Type; + +// The operation +export const leaveGuild = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: LeaveGuildInput, + outputSchema: LeaveGuildOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/leaveLobby.ts b/packages/discord/src/operations/leaveLobby.ts new file mode 100644 index 000000000..2cf6aa192 --- /dev/null +++ b/packages/discord/src/operations/leaveLobby.ts @@ -0,0 +1,21 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const LeaveLobbyInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + lobby_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "DELETE", path: "/lobbies/{lobby_id}/members/@me" })); +export type LeaveLobbyInput = typeof LeaveLobbyInput.Type; + +// Output Schema +export const LeaveLobbyOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type LeaveLobbyOutput = typeof LeaveLobbyOutput.Type; + +// The operation +export const leaveLobby = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: LeaveLobbyInput, + outputSchema: LeaveLobbyOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/leaveThread.ts b/packages/discord/src/operations/leaveThread.ts new file mode 100644 index 000000000..51a946581 --- /dev/null +++ b/packages/discord/src/operations/leaveThread.ts @@ -0,0 +1,26 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const LeaveThreadInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), +}).pipe( + T.Http({ + method: "DELETE", + path: "/channels/{channel_id}/thread-members/@me", + }), +); +export type LeaveThreadInput = typeof LeaveThreadInput.Type; + +// Output Schema +export const LeaveThreadOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type LeaveThreadOutput = typeof LeaveThreadOutput.Type; + +// The operation +export const leaveThread = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: LeaveThreadInput, + outputSchema: LeaveThreadOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/listApplicationCommands.ts b/packages/discord/src/operations/listApplicationCommands.ts new file mode 100644 index 000000000..9d0483f9c --- /dev/null +++ b/packages/discord/src/operations/listApplicationCommands.ts @@ -0,0 +1,54 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListApplicationCommandsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + with_localizations: Schema.optional(Schema.Boolean), + }).pipe( + T.Http({ method: "GET", path: "/applications/{application_id}/commands" }), + ); +export type ListApplicationCommandsInput = + typeof ListApplicationCommandsInput.Type; + +// Output Schema +export const ListApplicationCommandsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + id: Schema.String, + application_id: Schema.String, + version: Schema.String, + default_member_permissions: Schema.NullOr(Schema.String), + type: Schema.Unknown, + name: Schema.String, + name_localized: Schema.optional(Schema.String), + name_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + description: Schema.String, + description_localized: Schema.optional(Schema.String), + description_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + guild_id: Schema.optional(Schema.String), + dm_permission: Schema.optional(Schema.Boolean), + contexts: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + integration_types: Schema.optional(Schema.Array(Schema.Unknown)), + options: Schema.optional(Schema.Array(Schema.Unknown)), + nsfw: Schema.optional(Schema.Boolean), + }), + ); +export type ListApplicationCommandsOutput = + typeof ListApplicationCommandsOutput.Type; + +// The operation +export const listApplicationCommands = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: ListApplicationCommandsInput, + outputSchema: ListApplicationCommandsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/listApplicationEmojis.ts b/packages/discord/src/operations/listApplicationEmojis.ts new file mode 100644 index 000000000..e4057858d --- /dev/null +++ b/packages/discord/src/operations/listApplicationEmojis.ts @@ -0,0 +1,58 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListApplicationEmojisInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ method: "GET", path: "/applications/{application_id}/emojis" }), + ); +export type ListApplicationEmojisInput = typeof ListApplicationEmojisInput.Type; + +// Output Schema +export const ListApplicationEmojisOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + items: Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + roles: Schema.Array(Schema.String), + require_colons: Schema.Boolean, + managed: Schema.Boolean, + animated: Schema.Boolean, + available: Schema.Boolean, + }), + ), + }); +export type ListApplicationEmojisOutput = + typeof ListApplicationEmojisOutput.Type; + +// The operation +export const listApplicationEmojis = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: ListApplicationEmojisInput, + outputSchema: ListApplicationEmojisOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/listAutoModerationRules.ts b/packages/discord/src/operations/listAutoModerationRules.ts new file mode 100644 index 000000000..77915d37b --- /dev/null +++ b/packages/discord/src/operations/listAutoModerationRules.ts @@ -0,0 +1,29 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListAutoModerationRulesInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ method: "GET", path: "/guilds/{guild_id}/auto-moderation/rules" }), + ); +export type ListAutoModerationRulesInput = + typeof ListAutoModerationRulesInput.Type; + +// Output Schema +export const ListAutoModerationRulesOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array(Schema.Unknown); +export type ListAutoModerationRulesOutput = + typeof ListAutoModerationRulesOutput.Type; + +// The operation +export const listAutoModerationRules = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: ListAutoModerationRulesInput, + outputSchema: ListAutoModerationRulesOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/listChannelInvites.ts b/packages/discord/src/operations/listChannelInvites.ts new file mode 100644 index 000000000..26926343e --- /dev/null +++ b/packages/discord/src/operations/listChannelInvites.ts @@ -0,0 +1,23 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListChannelInvitesInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + }).pipe(T.Http({ method: "GET", path: "/channels/{channel_id}/invites" })); +export type ListChannelInvitesInput = typeof ListChannelInvitesInput.Type; + +// Output Schema +export const ListChannelInvitesOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array(Schema.Unknown); +export type ListChannelInvitesOutput = typeof ListChannelInvitesOutput.Type; + +// The operation +export const listChannelInvites = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListChannelInvitesInput, + outputSchema: ListChannelInvitesOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/listChannelWebhooks.ts b/packages/discord/src/operations/listChannelWebhooks.ts new file mode 100644 index 000000000..0b2e57c56 --- /dev/null +++ b/packages/discord/src/operations/listChannelWebhooks.ts @@ -0,0 +1,23 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListChannelWebhooksInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + }).pipe(T.Http({ method: "GET", path: "/channels/{channel_id}/webhooks" })); +export type ListChannelWebhooksInput = typeof ListChannelWebhooksInput.Type; + +// Output Schema +export const ListChannelWebhooksOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array(Schema.Unknown); +export type ListChannelWebhooksOutput = typeof ListChannelWebhooksOutput.Type; + +// The operation +export const listChannelWebhooks = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListChannelWebhooksInput, + outputSchema: ListChannelWebhooksOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/listGuildApplicationCommandPermissions.ts b/packages/discord/src/operations/listGuildApplicationCommandPermissions.ts new file mode 100644 index 000000000..ade2dec85 --- /dev/null +++ b/packages/discord/src/operations/listGuildApplicationCommandPermissions.ts @@ -0,0 +1,45 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListGuildApplicationCommandPermissionsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + guild_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "GET", + path: "/applications/{application_id}/guilds/{guild_id}/commands/permissions", + }), + ); +export type ListGuildApplicationCommandPermissionsInput = + typeof ListGuildApplicationCommandPermissionsInput.Type; + +// Output Schema +export const ListGuildApplicationCommandPermissionsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + id: Schema.String, + application_id: Schema.String, + guild_id: Schema.String, + permissions: Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + permission: Schema.Boolean, + }), + ), + }), + ); +export type ListGuildApplicationCommandPermissionsOutput = + typeof ListGuildApplicationCommandPermissionsOutput.Type; + +// The operation +export const listGuildApplicationCommandPermissions = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListGuildApplicationCommandPermissionsInput, + outputSchema: ListGuildApplicationCommandPermissionsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/listGuildApplicationCommands.ts b/packages/discord/src/operations/listGuildApplicationCommands.ts new file mode 100644 index 000000000..39e97f2e5 --- /dev/null +++ b/packages/discord/src/operations/listGuildApplicationCommands.ts @@ -0,0 +1,57 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListGuildApplicationCommandsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + guild_id: Schema.String.pipe(T.PathParam()), + with_localizations: Schema.optional(Schema.Boolean), + }).pipe( + T.Http({ + method: "GET", + path: "/applications/{application_id}/guilds/{guild_id}/commands", + }), + ); +export type ListGuildApplicationCommandsInput = + typeof ListGuildApplicationCommandsInput.Type; + +// Output Schema +export const ListGuildApplicationCommandsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + id: Schema.String, + application_id: Schema.String, + version: Schema.String, + default_member_permissions: Schema.NullOr(Schema.String), + type: Schema.Unknown, + name: Schema.String, + name_localized: Schema.optional(Schema.String), + name_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + description: Schema.String, + description_localized: Schema.optional(Schema.String), + description_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + guild_id: Schema.optional(Schema.String), + dm_permission: Schema.optional(Schema.Boolean), + contexts: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + integration_types: Schema.optional(Schema.Array(Schema.Unknown)), + options: Schema.optional(Schema.Array(Schema.Unknown)), + nsfw: Schema.optional(Schema.Boolean), + }), + ); +export type ListGuildApplicationCommandsOutput = + typeof ListGuildApplicationCommandsOutput.Type; + +// The operation +export const listGuildApplicationCommands = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListGuildApplicationCommandsInput, + outputSchema: ListGuildApplicationCommandsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/listGuildAuditLogEntries.ts b/packages/discord/src/operations/listGuildAuditLogEntries.ts new file mode 100644 index 000000000..fe8da1eff --- /dev/null +++ b/packages/discord/src/operations/listGuildAuditLogEntries.ts @@ -0,0 +1,172 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListGuildAuditLogEntriesInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + user_id: Schema.optional(Schema.String), + target_id: Schema.optional(Schema.String), + action_type: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), + after: Schema.optional(Schema.String), + limit: Schema.optional(Schema.Number), + }).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/audit-logs" })); +export type ListGuildAuditLogEntriesInput = + typeof ListGuildAuditLogEntriesInput.Type; + +// Output Schema +export const ListGuildAuditLogEntriesOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + audit_log_entries: Schema.Array( + Schema.Struct({ + id: Schema.String, + action_type: Schema.Unknown, + user_id: Schema.Unknown, + target_id: Schema.Unknown, + changes: Schema.optional( + Schema.Array( + Schema.Struct({ + key: Schema.NullOr(Schema.String), + new_value: Schema.optional(Schema.Unknown), + old_value: Schema.optional(Schema.Unknown), + }), + ), + ), + options: Schema.optional(Schema.Record(Schema.String, Schema.String)), + reason: Schema.optional(Schema.String), + }), + ), + users: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + integrations: Schema.Array(Schema.Unknown), + webhooks: Schema.Array(Schema.Unknown), + guild_scheduled_events: Schema.Array(Schema.Unknown), + threads: Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + application_commands: Schema.Array( + Schema.Struct({ + id: Schema.String, + application_id: Schema.String, + version: Schema.String, + default_member_permissions: Schema.NullOr(Schema.String), + type: Schema.Unknown, + name: Schema.String, + name_localized: Schema.optional(Schema.String), + name_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + description: Schema.String, + description_localized: Schema.optional(Schema.String), + description_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + guild_id: Schema.optional(Schema.String), + dm_permission: Schema.optional(Schema.Boolean), + contexts: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + integration_types: Schema.optional(Schema.Array(Schema.Unknown)), + options: Schema.optional(Schema.Array(Schema.Unknown)), + nsfw: Schema.optional(Schema.Boolean), + }), + ), + auto_moderation_rules: Schema.Array(Schema.Unknown), + }); +export type ListGuildAuditLogEntriesOutput = + typeof ListGuildAuditLogEntriesOutput.Type; + +// The operation +export const listGuildAuditLogEntries = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: ListGuildAuditLogEntriesInput, + outputSchema: ListGuildAuditLogEntriesOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/listGuildBans.ts b/packages/discord/src/operations/listGuildBans.ts new file mode 100644 index 000000000..73a5f8a8f --- /dev/null +++ b/packages/discord/src/operations/listGuildBans.ts @@ -0,0 +1,44 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListGuildBansInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + limit: Schema.optional(Schema.Number), + before: Schema.optional(Schema.String), + after: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/bans" })); +export type ListGuildBansInput = typeof ListGuildBansInput.Type; + +// Output Schema +export const ListGuildBansOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + reason: Schema.NullOr(Schema.String), + }), +); +export type ListGuildBansOutput = typeof ListGuildBansOutput.Type; + +// The operation +export const listGuildBans = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListGuildBansInput, + outputSchema: ListGuildBansOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/listGuildChannels.ts b/packages/discord/src/operations/listGuildChannels.ts new file mode 100644 index 000000000..58156eedc --- /dev/null +++ b/packages/discord/src/operations/listGuildChannels.ts @@ -0,0 +1,25 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListGuildChannelsInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + guild_id: Schema.String.pipe(T.PathParam()), + }, +).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/channels" })); +export type ListGuildChannelsInput = typeof ListGuildChannelsInput.Type; + +// Output Schema +export const ListGuildChannelsOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Unknown, +); +export type ListGuildChannelsOutput = typeof ListGuildChannelsOutput.Type; + +// The operation +export const listGuildChannels = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListGuildChannelsInput, + outputSchema: ListGuildChannelsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/listGuildEmojis.ts b/packages/discord/src/operations/listGuildEmojis.ts new file mode 100644 index 000000000..c1b18d09e --- /dev/null +++ b/packages/discord/src/operations/listGuildEmojis.ts @@ -0,0 +1,49 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListGuildEmojisInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/emojis" })); +export type ListGuildEmojisInput = typeof ListGuildEmojisInput.Type; + +// Output Schema +export const ListGuildEmojisOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + roles: Schema.Array(Schema.String), + require_colons: Schema.Boolean, + managed: Schema.Boolean, + animated: Schema.Boolean, + available: Schema.Boolean, + }), +); +export type ListGuildEmojisOutput = typeof ListGuildEmojisOutput.Type; + +// The operation +export const listGuildEmojis = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListGuildEmojisInput, + outputSchema: ListGuildEmojisOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/listGuildIntegrations.ts b/packages/discord/src/operations/listGuildIntegrations.ts new file mode 100644 index 000000000..5116d440d --- /dev/null +++ b/packages/discord/src/operations/listGuildIntegrations.ts @@ -0,0 +1,26 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListGuildIntegrationsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + }).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/integrations" })); +export type ListGuildIntegrationsInput = typeof ListGuildIntegrationsInput.Type; + +// Output Schema +export const ListGuildIntegrationsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array(Schema.Unknown); +export type ListGuildIntegrationsOutput = + typeof ListGuildIntegrationsOutput.Type; + +// The operation +export const listGuildIntegrations = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: ListGuildIntegrationsInput, + outputSchema: ListGuildIntegrationsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/listGuildInvites.ts b/packages/discord/src/operations/listGuildInvites.ts new file mode 100644 index 000000000..595155a3c --- /dev/null +++ b/packages/discord/src/operations/listGuildInvites.ts @@ -0,0 +1,23 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListGuildInvitesInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/invites" })); +export type ListGuildInvitesInput = typeof ListGuildInvitesInput.Type; + +// Output Schema +export const ListGuildInvitesOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Unknown, +); +export type ListGuildInvitesOutput = typeof ListGuildInvitesOutput.Type; + +// The operation +export const listGuildInvites = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListGuildInvitesInput, + outputSchema: ListGuildInvitesOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/listGuildMembers.ts b/packages/discord/src/operations/listGuildMembers.ts new file mode 100644 index 000000000..dcf25bd75 --- /dev/null +++ b/packages/discord/src/operations/listGuildMembers.ts @@ -0,0 +1,55 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListGuildMembersInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.Number), +}).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/members" })); +export type ListGuildMembersInput = typeof ListGuildMembersInput.Type; + +// Output Schema +export const ListGuildMembersOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), +); +export type ListGuildMembersOutput = typeof ListGuildMembersOutput.Type; + +// The operation +export const listGuildMembers = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListGuildMembersInput, + outputSchema: ListGuildMembersOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/listGuildRoles.ts b/packages/discord/src/operations/listGuildRoles.ts new file mode 100644 index 000000000..27207c8e7 --- /dev/null +++ b/packages/discord/src/operations/listGuildRoles.ts @@ -0,0 +1,51 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListGuildRolesInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/roles" })); +export type ListGuildRolesInput = typeof ListGuildRolesInput.Type; + +// Output Schema +export const ListGuildRolesOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), +); +export type ListGuildRolesOutput = typeof ListGuildRolesOutput.Type; + +// The operation +export const listGuildRoles = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListGuildRolesInput, + outputSchema: ListGuildRolesOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/listGuildScheduledEventUsers.ts b/packages/discord/src/operations/listGuildScheduledEventUsers.ts new file mode 100644 index 000000000..74ac43a57 --- /dev/null +++ b/packages/discord/src/operations/listGuildScheduledEventUsers.ts @@ -0,0 +1,92 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListGuildScheduledEventUsersInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + guild_scheduled_event_id: Schema.String.pipe(T.PathParam()), + with_member: Schema.optional(Schema.Boolean), + limit: Schema.optional(Schema.Number), + before: Schema.optional(Schema.String), + after: Schema.optional(Schema.String), + }).pipe( + T.Http({ + method: "GET", + path: "/guilds/{guild_id}/scheduled-events/{guild_scheduled_event_id}/users", + }), + ); +export type ListGuildScheduledEventUsersInput = + typeof ListGuildScheduledEventUsersInput.Type; + +// Output Schema +export const ListGuildScheduledEventUsersOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + guild_scheduled_event_id: Schema.String, + user_id: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ); +export type ListGuildScheduledEventUsersOutput = + typeof ListGuildScheduledEventUsersOutput.Type; + +// The operation +export const listGuildScheduledEventUsers = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListGuildScheduledEventUsersInput, + outputSchema: ListGuildScheduledEventUsersOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/listGuildScheduledEvents.ts b/packages/discord/src/operations/listGuildScheduledEvents.ts new file mode 100644 index 000000000..9c62db849 --- /dev/null +++ b/packages/discord/src/operations/listGuildScheduledEvents.ts @@ -0,0 +1,30 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListGuildScheduledEventsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + with_user_count: Schema.optional(Schema.Boolean), + }).pipe( + T.Http({ method: "GET", path: "/guilds/{guild_id}/scheduled-events" }), + ); +export type ListGuildScheduledEventsInput = + typeof ListGuildScheduledEventsInput.Type; + +// Output Schema +export const ListGuildScheduledEventsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array(Schema.Unknown); +export type ListGuildScheduledEventsOutput = + typeof ListGuildScheduledEventsOutput.Type; + +// The operation +export const listGuildScheduledEvents = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: ListGuildScheduledEventsInput, + outputSchema: ListGuildScheduledEventsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/listGuildSoundboardSounds.ts b/packages/discord/src/operations/listGuildSoundboardSounds.ts new file mode 100644 index 000000000..2a41fa683 --- /dev/null +++ b/packages/discord/src/operations/listGuildSoundboardSounds.ts @@ -0,0 +1,59 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListGuildSoundboardSoundsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ method: "GET", path: "/guilds/{guild_id}/soundboard-sounds" }), + ); +export type ListGuildSoundboardSoundsInput = + typeof ListGuildSoundboardSoundsInput.Type; + +// Output Schema +export const ListGuildSoundboardSoundsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + items: Schema.Array( + Schema.Struct({ + name: Schema.String, + sound_id: Schema.String, + volume: Schema.Number, + emoji_id: Schema.Unknown, + emoji_name: Schema.NullOr(Schema.String), + guild_id: Schema.optional(Schema.String), + available: Schema.Boolean, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + }); +export type ListGuildSoundboardSoundsOutput = + typeof ListGuildSoundboardSoundsOutput.Type; + +// The operation +export const listGuildSoundboardSounds = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: ListGuildSoundboardSoundsInput, + outputSchema: ListGuildSoundboardSoundsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/listGuildStickers.ts b/packages/discord/src/operations/listGuildStickers.ts new file mode 100644 index 000000000..507138fbc --- /dev/null +++ b/packages/discord/src/operations/listGuildStickers.ts @@ -0,0 +1,52 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListGuildStickersInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + guild_id: Schema.String.pipe(T.PathParam()), + }, +).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/stickers" })); +export type ListGuildStickersInput = typeof ListGuildStickersInput.Type; + +// Output Schema +export const ListGuildStickersOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + tags: Schema.String, + type: Schema.Unknown, + format_type: Schema.Unknown, + description: Schema.NullOr(Schema.String), + available: Schema.Boolean, + guild_id: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), +); +export type ListGuildStickersOutput = typeof ListGuildStickersOutput.Type; + +// The operation +export const listGuildStickers = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListGuildStickersInput, + outputSchema: ListGuildStickersOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/listGuildTemplates.ts b/packages/discord/src/operations/listGuildTemplates.ts new file mode 100644 index 000000000..70bc977eb --- /dev/null +++ b/packages/discord/src/operations/listGuildTemplates.ts @@ -0,0 +1,104 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListGuildTemplatesInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + }).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/templates" })); +export type ListGuildTemplatesInput = typeof ListGuildTemplatesInput.Type; + +// Output Schema +export const ListGuildTemplatesOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + code: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + usage_count: Schema.Number, + creator_id: Schema.String, + creator: Schema.Unknown, + created_at: Schema.String, + updated_at: Schema.String, + source_guild_id: Schema.String, + serialized_source_guild: Schema.Struct({ + name: Schema.String, + description: Schema.NullOr(Schema.String), + region: Schema.NullOr(Schema.String), + verification_level: Schema.Unknown, + default_message_notifications: Schema.Unknown, + explicit_content_filter: Schema.Unknown, + preferred_locale: Schema.Unknown, + afk_channel_id: Schema.Unknown, + afk_timeout: Schema.Unknown, + system_channel_id: Schema.Unknown, + system_channel_flags: Schema.Number, + roles: Schema.Array( + Schema.Struct({ + id: Schema.Number, + name: Schema.String, + permissions: Schema.String, + color: Schema.Number, + colors: Schema.Unknown, + hoist: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + }), + ), + channels: Schema.Array( + Schema.Struct({ + id: Schema.NullOr(Schema.Number), + type: Schema.Unknown, + name: Schema.NullOr(Schema.String), + position: Schema.NullOr(Schema.Number), + topic: Schema.NullOr(Schema.String), + bitrate: Schema.Number, + user_limit: Schema.Number, + nsfw: Schema.Boolean, + rate_limit_per_user: Schema.Number, + parent_id: Schema.Unknown, + default_auto_archive_duration: Schema.Unknown, + permission_overwrites: Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + allow: Schema.String, + deny: Schema.String, + }), + ), + available_tags: Schema.NullOr( + Schema.Array( + Schema.Struct({ + id: Schema.NullOr(Schema.Number), + name: Schema.String, + emoji_id: Schema.Unknown, + emoji_name: Schema.NullOr(Schema.String), + moderated: Schema.NullOr(Schema.Boolean), + }), + ), + ), + template: Schema.String, + default_reaction_emoji: Schema.Unknown, + default_thread_rate_limit_per_user: Schema.NullOr(Schema.Number), + default_sort_order: Schema.Unknown, + default_forum_layout: Schema.Unknown, + default_tag_setting: Schema.Unknown, + icon_emoji: Schema.Unknown, + theme_color: Schema.NullOr(Schema.Number), + }), + ), + }), + is_dirty: Schema.NullOr(Schema.Boolean), + }), + ); +export type ListGuildTemplatesOutput = typeof ListGuildTemplatesOutput.Type; + +// The operation +export const listGuildTemplates = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListGuildTemplatesInput, + outputSchema: ListGuildTemplatesOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/listGuildVoiceRegions.ts b/packages/discord/src/operations/listGuildVoiceRegions.ts new file mode 100644 index 000000000..aac292144 --- /dev/null +++ b/packages/discord/src/operations/listGuildVoiceRegions.ts @@ -0,0 +1,34 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListGuildVoiceRegionsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + }).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/regions" })); +export type ListGuildVoiceRegionsInput = typeof ListGuildVoiceRegionsInput.Type; + +// Output Schema +export const ListGuildVoiceRegionsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + custom: Schema.Boolean, + deprecated: Schema.Boolean, + optimal: Schema.Boolean, + }), + ); +export type ListGuildVoiceRegionsOutput = + typeof ListGuildVoiceRegionsOutput.Type; + +// The operation +export const listGuildVoiceRegions = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: ListGuildVoiceRegionsInput, + outputSchema: ListGuildVoiceRegionsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/listMessageReactionsByEmoji.ts b/packages/discord/src/operations/listMessageReactionsByEmoji.ts new file mode 100644 index 000000000..06d2a2b20 --- /dev/null +++ b/packages/discord/src/operations/listMessageReactionsByEmoji.ts @@ -0,0 +1,54 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListMessageReactionsByEmojiInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), + emoji_name: Schema.String.pipe(T.PathParam()), + after: Schema.optional(Schema.String), + limit: Schema.optional(Schema.Number), + type: Schema.optional(Schema.String), + }).pipe( + T.Http({ + method: "GET", + path: "/channels/{channel_id}/messages/{message_id}/reactions/{emoji_name}", + }), + ); +export type ListMessageReactionsByEmojiInput = + typeof ListMessageReactionsByEmojiInput.Type; + +// Output Schema +export const ListMessageReactionsByEmojiOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ); +export type ListMessageReactionsByEmojiOutput = + typeof ListMessageReactionsByEmojiOutput.Type; + +// The operation +export const listMessageReactionsByEmoji = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: ListMessageReactionsByEmojiInput, + outputSchema: ListMessageReactionsByEmojiOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/listMessages.ts b/packages/discord/src/operations/listMessages.ts new file mode 100644 index 000000000..70f732e89 --- /dev/null +++ b/packages/discord/src/operations/listMessages.ts @@ -0,0 +1,856 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListMessagesInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + around: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), + after: Schema.optional(Schema.String), + limit: Schema.optional(Schema.Number), +}).pipe(T.Http({ method: "GET", path: "/channels/{channel_id}/messages" })); +export type ListMessagesInput = typeof ListMessagesInput.Type; + +// Output Schema +export const ListMessagesOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + id: Schema.String, + channel_id: Schema.String, + author: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + pinned: Schema.Boolean, + mention_everyone: Schema.Boolean, + tts: Schema.Boolean, + call: Schema.optional( + Schema.Struct({ + ended_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + participants: Schema.Array(Schema.String), + }), + ), + activity: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + party_id: Schema.optional(Schema.String), + }), + ), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + application_id: Schema.optional(Schema.String), + interaction: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + name_localized: Schema.optional(Schema.String), + }), + ), + nonce: Schema.optional(Schema.Unknown), + webhook_id: Schema.optional(Schema.String), + message_reference: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + channel_id: Schema.String, + message_id: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + }), + ), + thread: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + mention_channels: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + type: Schema.Unknown, + guild_id: Schema.String, + }), + ), + ), + role_subscription_data: Schema.optional( + Schema.Struct({ + role_subscription_listing_id: Schema.String, + tier_name: Schema.String, + total_months_subscribed: Schema.Number, + is_renewal: Schema.Boolean, + }), + ), + purchase_notification: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + guild_product_purchase: Schema.optional( + Schema.Struct({ + listing_id: Schema.String, + product_name: Schema.String, + }), + ), + }), + ), + position: Schema.optional(Schema.Number), + resolved: Schema.optional( + Schema.Struct({ + users: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + ), + members: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + channels: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + ), + roles: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ), + ), + ), + }), + ), + poll: Schema.optional( + Schema.Struct({ + question: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + answers: Schema.Array( + Schema.Struct({ + answer_id: Schema.Number, + poll_media: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + }), + ), + expiry: Schema.String, + allow_multiselect: Schema.Boolean, + layout_type: Schema.Unknown, + results: Schema.Struct({ + answer_counts: Schema.Array( + Schema.Struct({ + id: Schema.Number, + count: Schema.Number, + me_voted: Schema.Boolean, + }), + ), + is_finalized: Schema.Boolean, + }), + }), + ), + shared_client_theme: Schema.optional( + Schema.Struct({ + colors: Schema.Array(Schema.String), + gradient_angle: Schema.Number, + base_mix: Schema.Number, + base_theme: Schema.Unknown, + }), + ), + interaction_metadata: Schema.optional(Schema.Unknown), + message_snapshots: Schema.optional( + Schema.Array( + Schema.Struct({ + message: Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + }), + }), + ), + ), + reactions: Schema.optional( + Schema.Array( + Schema.Struct({ + emoji: Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + count: Schema.Number, + count_details: Schema.Struct({ + burst: Schema.Number, + normal: Schema.Number, + }), + burst_colors: Schema.Array(Schema.String), + me_burst: Schema.Boolean, + me: Schema.Boolean, + }), + ), + ), + referenced_message: Schema.optional(Schema.Unknown), + }), +); +export type ListMessagesOutput = typeof ListMessagesOutput.Type; + +// The operation +export const listMessages = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListMessagesInput, + outputSchema: ListMessagesOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/listMyConnections.ts b/packages/discord/src/operations/listMyConnections.ts new file mode 100644 index 000000000..5f551c58b --- /dev/null +++ b/packages/discord/src/operations/listMyConnections.ts @@ -0,0 +1,50 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListMyConnectionsInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + {}, +).pipe(T.Http({ method: "GET", path: "/users/@me/connections" })); +export type ListMyConnectionsInput = typeof ListMyConnectionsInput.Type; + +// Output Schema +export const ListMyConnectionsOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.NullOr(Schema.String), + type: Schema.Unknown, + friend_sync: Schema.Boolean, + integrations: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + account: Schema.Struct({ + id: Schema.String, + name: Schema.NullOr(Schema.String), + }), + guild: Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + }), + }), + ), + ), + show_activity: Schema.Boolean, + two_way_link: Schema.Boolean, + verified: Schema.Boolean, + visibility: Schema.Unknown, + revoked: Schema.optional(Schema.Boolean), + }), +); +export type ListMyConnectionsOutput = typeof ListMyConnectionsOutput.Type; + +// The operation +export const listMyConnections = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListMyConnectionsInput, + outputSchema: ListMyConnectionsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/listMyGuilds.ts b/packages/discord/src/operations/listMyGuilds.ts new file mode 100644 index 000000000..ab587f491 --- /dev/null +++ b/packages/discord/src/operations/listMyGuilds.ts @@ -0,0 +1,36 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListMyGuildsInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + before: Schema.optional(Schema.String), + after: Schema.optional(Schema.String), + limit: Schema.optional(Schema.Number), + with_counts: Schema.optional(Schema.Boolean), +}).pipe(T.Http({ method: "GET", path: "/users/@me/guilds" })); +export type ListMyGuildsInput = typeof ListMyGuildsInput.Type; + +// Output Schema +export const ListMyGuildsOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + banner: Schema.NullOr(Schema.String), + owner: Schema.Boolean, + permissions: Schema.String, + features: Schema.Array(Schema.Unknown), + approximate_member_count: Schema.optional(Schema.NullOr(Schema.Number)), + approximate_presence_count: Schema.optional(Schema.NullOr(Schema.Number)), + }), +); +export type ListMyGuildsOutput = typeof ListMyGuildsOutput.Type; + +// The operation +export const listMyGuilds = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListMyGuildsInput, + outputSchema: ListMyGuildsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/listMyPrivateArchivedThreads.ts b/packages/discord/src/operations/listMyPrivateArchivedThreads.ts new file mode 100644 index 000000000..95c8afdf2 --- /dev/null +++ b/packages/discord/src/operations/listMyPrivateArchivedThreads.ts @@ -0,0 +1,1012 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListMyPrivateArchivedThreadsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + before: Schema.optional(Schema.String), + limit: Schema.optional(Schema.Number), + }).pipe( + T.Http({ + method: "GET", + path: "/channels/{channel_id}/users/@me/threads/archived/private", + }), + ); +export type ListMyPrivateArchivedThreadsInput = + typeof ListMyPrivateArchivedThreadsInput.Type; + +// Output Schema +export const ListMyPrivateArchivedThreadsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + threads: Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + members: Schema.Array( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + has_more: Schema.Boolean, + first_messages: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + id: Schema.String, + channel_id: Schema.String, + author: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + pinned: Schema.Boolean, + mention_everyone: Schema.Boolean, + tts: Schema.Boolean, + call: Schema.optional( + Schema.Struct({ + ended_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + participants: Schema.Array(Schema.String), + }), + ), + activity: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + party_id: Schema.optional(Schema.String), + }), + ), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + application_id: Schema.optional(Schema.String), + interaction: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + name_localized: Schema.optional(Schema.String), + }), + ), + nonce: Schema.optional(Schema.Unknown), + webhook_id: Schema.optional(Schema.String), + message_reference: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + channel_id: Schema.String, + message_id: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + }), + ), + thread: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr( + Schema.String, + ), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + mention_channels: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + type: Schema.Unknown, + guild_id: Schema.String, + }), + ), + ), + role_subscription_data: Schema.optional( + Schema.Struct({ + role_subscription_listing_id: Schema.String, + tier_name: Schema.String, + total_months_subscribed: Schema.Number, + is_renewal: Schema.Boolean, + }), + ), + purchase_notification: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + guild_product_purchase: Schema.optional( + Schema.Struct({ + listing_id: Schema.String, + product_name: Schema.String, + }), + ), + }), + ), + position: Schema.optional(Schema.Number), + resolved: Schema.optional( + Schema.Struct({ + users: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + ), + members: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr( + Schema.String, + ), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + channels: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + ), + roles: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional( + Schema.String, + ), + available_for_purchase: Schema.optional( + Schema.Unknown, + ), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ), + ), + ), + }), + ), + poll: Schema.optional( + Schema.Struct({ + question: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + answers: Schema.Array( + Schema.Struct({ + answer_id: Schema.Number, + poll_media: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + }), + ), + expiry: Schema.String, + allow_multiselect: Schema.Boolean, + layout_type: Schema.Unknown, + results: Schema.Struct({ + answer_counts: Schema.Array( + Schema.Struct({ + id: Schema.Number, + count: Schema.Number, + me_voted: Schema.Boolean, + }), + ), + is_finalized: Schema.Boolean, + }), + }), + ), + shared_client_theme: Schema.optional( + Schema.Struct({ + colors: Schema.Array(Schema.String), + gradient_angle: Schema.Number, + base_mix: Schema.Number, + base_theme: Schema.Unknown, + }), + ), + interaction_metadata: Schema.optional(Schema.Unknown), + message_snapshots: Schema.optional( + Schema.Array( + Schema.Struct({ + message: Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional( + Schema.NullOr(Schema.String), + ), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional( + Schema.Unknown, + ), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional( + Schema.Array(Schema.String), + ), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional( + Schema.Boolean, + ), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional( + Schema.NullOr(Schema.String), + ), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional( + Schema.Unknown, + ), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + }), + }), + ), + ), + reactions: Schema.optional( + Schema.Array( + Schema.Struct({ + emoji: Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + count: Schema.Number, + count_details: Schema.Struct({ + burst: Schema.Number, + normal: Schema.Number, + }), + burst_colors: Schema.Array(Schema.String), + me_burst: Schema.Boolean, + me: Schema.Boolean, + }), + ), + ), + referenced_message: Schema.optional(Schema.Unknown), + }), + ), + ), + }); +export type ListMyPrivateArchivedThreadsOutput = + typeof ListMyPrivateArchivedThreadsOutput.Type; + +// The operation +export const listMyPrivateArchivedThreads = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListMyPrivateArchivedThreadsInput, + outputSchema: ListMyPrivateArchivedThreadsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/listPins.ts b/packages/discord/src/operations/listPins.ts new file mode 100644 index 000000000..ca2b5622b --- /dev/null +++ b/packages/discord/src/operations/listPins.ts @@ -0,0 +1,872 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListPinsInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + before: Schema.optional(Schema.String), + limit: Schema.optional(Schema.Number), +}).pipe( + T.Http({ method: "GET", path: "/channels/{channel_id}/messages/pins" }), +); +export type ListPinsInput = typeof ListPinsInput.Type; + +// Output Schema +export const ListPinsOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + items: Schema.Array( + Schema.Struct({ + pinned_at: Schema.String, + message: Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + id: Schema.String, + channel_id: Schema.String, + author: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + pinned: Schema.Boolean, + mention_everyone: Schema.Boolean, + tts: Schema.Boolean, + call: Schema.optional( + Schema.Struct({ + ended_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + participants: Schema.Array(Schema.String), + }), + ), + activity: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + party_id: Schema.optional(Schema.String), + }), + ), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + application_id: Schema.optional(Schema.String), + interaction: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + name_localized: Schema.optional(Schema.String), + }), + ), + nonce: Schema.optional(Schema.Unknown), + webhook_id: Schema.optional(Schema.String), + message_reference: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + channel_id: Schema.String, + message_id: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + }), + ), + thread: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + mention_channels: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + type: Schema.Unknown, + guild_id: Schema.String, + }), + ), + ), + role_subscription_data: Schema.optional( + Schema.Struct({ + role_subscription_listing_id: Schema.String, + tier_name: Schema.String, + total_months_subscribed: Schema.Number, + is_renewal: Schema.Boolean, + }), + ), + purchase_notification: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + guild_product_purchase: Schema.optional( + Schema.Struct({ + listing_id: Schema.String, + product_name: Schema.String, + }), + ), + }), + ), + position: Schema.optional(Schema.Number), + resolved: Schema.optional( + Schema.Struct({ + users: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + ), + members: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + channels: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + ), + roles: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ), + ), + ), + }), + ), + poll: Schema.optional( + Schema.Struct({ + question: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + answers: Schema.Array( + Schema.Struct({ + answer_id: Schema.Number, + poll_media: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + }), + ), + expiry: Schema.String, + allow_multiselect: Schema.Boolean, + layout_type: Schema.Unknown, + results: Schema.Struct({ + answer_counts: Schema.Array( + Schema.Struct({ + id: Schema.Number, + count: Schema.Number, + me_voted: Schema.Boolean, + }), + ), + is_finalized: Schema.Boolean, + }), + }), + ), + shared_client_theme: Schema.optional( + Schema.Struct({ + colors: Schema.Array(Schema.String), + gradient_angle: Schema.Number, + base_mix: Schema.Number, + base_theme: Schema.Unknown, + }), + ), + interaction_metadata: Schema.optional(Schema.Unknown), + message_snapshots: Schema.optional( + Schema.Array( + Schema.Struct({ + message: Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional( + Schema.NullOr(Schema.String), + ), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional( + Schema.Unknown, + ), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional( + Schema.Array(Schema.String), + ), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional( + Schema.Unknown, + ), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + }), + }), + ), + ), + reactions: Schema.optional( + Schema.Array( + Schema.Struct({ + emoji: Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + count: Schema.Number, + count_details: Schema.Struct({ + burst: Schema.Number, + normal: Schema.Number, + }), + burst_colors: Schema.Array(Schema.String), + me_burst: Schema.Boolean, + me: Schema.Boolean, + }), + ), + ), + referenced_message: Schema.optional(Schema.Unknown), + }), + }), + ), + has_more: Schema.Boolean, +}); +export type ListPinsOutput = typeof ListPinsOutput.Type; + +// The operation +export const listPins = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListPinsInput, + outputSchema: ListPinsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/listPrivateArchivedThreads.ts b/packages/discord/src/operations/listPrivateArchivedThreads.ts new file mode 100644 index 000000000..e948b2a05 --- /dev/null +++ b/packages/discord/src/operations/listPrivateArchivedThreads.ts @@ -0,0 +1,1013 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListPrivateArchivedThreadsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + before: Schema.optional(Schema.String), + limit: Schema.optional(Schema.Number), + }).pipe( + T.Http({ + method: "GET", + path: "/channels/{channel_id}/threads/archived/private", + }), + ); +export type ListPrivateArchivedThreadsInput = + typeof ListPrivateArchivedThreadsInput.Type; + +// Output Schema +export const ListPrivateArchivedThreadsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + threads: Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + members: Schema.Array( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + has_more: Schema.Boolean, + first_messages: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + id: Schema.String, + channel_id: Schema.String, + author: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + pinned: Schema.Boolean, + mention_everyone: Schema.Boolean, + tts: Schema.Boolean, + call: Schema.optional( + Schema.Struct({ + ended_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + participants: Schema.Array(Schema.String), + }), + ), + activity: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + party_id: Schema.optional(Schema.String), + }), + ), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + application_id: Schema.optional(Schema.String), + interaction: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + name_localized: Schema.optional(Schema.String), + }), + ), + nonce: Schema.optional(Schema.Unknown), + webhook_id: Schema.optional(Schema.String), + message_reference: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + channel_id: Schema.String, + message_id: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + }), + ), + thread: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr( + Schema.String, + ), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + mention_channels: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + type: Schema.Unknown, + guild_id: Schema.String, + }), + ), + ), + role_subscription_data: Schema.optional( + Schema.Struct({ + role_subscription_listing_id: Schema.String, + tier_name: Schema.String, + total_months_subscribed: Schema.Number, + is_renewal: Schema.Boolean, + }), + ), + purchase_notification: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + guild_product_purchase: Schema.optional( + Schema.Struct({ + listing_id: Schema.String, + product_name: Schema.String, + }), + ), + }), + ), + position: Schema.optional(Schema.Number), + resolved: Schema.optional( + Schema.Struct({ + users: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + ), + members: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr( + Schema.String, + ), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + channels: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + ), + roles: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional( + Schema.String, + ), + available_for_purchase: Schema.optional( + Schema.Unknown, + ), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ), + ), + ), + }), + ), + poll: Schema.optional( + Schema.Struct({ + question: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + answers: Schema.Array( + Schema.Struct({ + answer_id: Schema.Number, + poll_media: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + }), + ), + expiry: Schema.String, + allow_multiselect: Schema.Boolean, + layout_type: Schema.Unknown, + results: Schema.Struct({ + answer_counts: Schema.Array( + Schema.Struct({ + id: Schema.Number, + count: Schema.Number, + me_voted: Schema.Boolean, + }), + ), + is_finalized: Schema.Boolean, + }), + }), + ), + shared_client_theme: Schema.optional( + Schema.Struct({ + colors: Schema.Array(Schema.String), + gradient_angle: Schema.Number, + base_mix: Schema.Number, + base_theme: Schema.Unknown, + }), + ), + interaction_metadata: Schema.optional(Schema.Unknown), + message_snapshots: Schema.optional( + Schema.Array( + Schema.Struct({ + message: Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional( + Schema.NullOr(Schema.String), + ), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional( + Schema.Unknown, + ), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional( + Schema.Array(Schema.String), + ), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional( + Schema.Boolean, + ), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional( + Schema.NullOr(Schema.String), + ), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional( + Schema.Unknown, + ), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + }), + }), + ), + ), + reactions: Schema.optional( + Schema.Array( + Schema.Struct({ + emoji: Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + count: Schema.Number, + count_details: Schema.Struct({ + burst: Schema.Number, + normal: Schema.Number, + }), + burst_colors: Schema.Array(Schema.String), + me_burst: Schema.Boolean, + me: Schema.Boolean, + }), + ), + ), + referenced_message: Schema.optional(Schema.Unknown), + }), + ), + ), + }); +export type ListPrivateArchivedThreadsOutput = + typeof ListPrivateArchivedThreadsOutput.Type; + +// The operation +export const listPrivateArchivedThreads = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: ListPrivateArchivedThreadsInput, + outputSchema: ListPrivateArchivedThreadsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/listPublicArchivedThreads.ts b/packages/discord/src/operations/listPublicArchivedThreads.ts new file mode 100644 index 000000000..e3be9e5d1 --- /dev/null +++ b/packages/discord/src/operations/listPublicArchivedThreads.ts @@ -0,0 +1,1013 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListPublicArchivedThreadsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + before: Schema.optional(Schema.String), + limit: Schema.optional(Schema.Number), + }).pipe( + T.Http({ + method: "GET", + path: "/channels/{channel_id}/threads/archived/public", + }), + ); +export type ListPublicArchivedThreadsInput = + typeof ListPublicArchivedThreadsInput.Type; + +// Output Schema +export const ListPublicArchivedThreadsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + threads: Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + members: Schema.Array( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + has_more: Schema.Boolean, + first_messages: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + id: Schema.String, + channel_id: Schema.String, + author: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + pinned: Schema.Boolean, + mention_everyone: Schema.Boolean, + tts: Schema.Boolean, + call: Schema.optional( + Schema.Struct({ + ended_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + participants: Schema.Array(Schema.String), + }), + ), + activity: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + party_id: Schema.optional(Schema.String), + }), + ), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + application_id: Schema.optional(Schema.String), + interaction: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + name_localized: Schema.optional(Schema.String), + }), + ), + nonce: Schema.optional(Schema.Unknown), + webhook_id: Schema.optional(Schema.String), + message_reference: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + channel_id: Schema.String, + message_id: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + }), + ), + thread: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr( + Schema.String, + ), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + mention_channels: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + type: Schema.Unknown, + guild_id: Schema.String, + }), + ), + ), + role_subscription_data: Schema.optional( + Schema.Struct({ + role_subscription_listing_id: Schema.String, + tier_name: Schema.String, + total_months_subscribed: Schema.Number, + is_renewal: Schema.Boolean, + }), + ), + purchase_notification: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + guild_product_purchase: Schema.optional( + Schema.Struct({ + listing_id: Schema.String, + product_name: Schema.String, + }), + ), + }), + ), + position: Schema.optional(Schema.Number), + resolved: Schema.optional( + Schema.Struct({ + users: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + ), + members: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr( + Schema.String, + ), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + channels: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + ), + roles: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional( + Schema.String, + ), + available_for_purchase: Schema.optional( + Schema.Unknown, + ), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ), + ), + ), + }), + ), + poll: Schema.optional( + Schema.Struct({ + question: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + answers: Schema.Array( + Schema.Struct({ + answer_id: Schema.Number, + poll_media: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + }), + ), + expiry: Schema.String, + allow_multiselect: Schema.Boolean, + layout_type: Schema.Unknown, + results: Schema.Struct({ + answer_counts: Schema.Array( + Schema.Struct({ + id: Schema.Number, + count: Schema.Number, + me_voted: Schema.Boolean, + }), + ), + is_finalized: Schema.Boolean, + }), + }), + ), + shared_client_theme: Schema.optional( + Schema.Struct({ + colors: Schema.Array(Schema.String), + gradient_angle: Schema.Number, + base_mix: Schema.Number, + base_theme: Schema.Unknown, + }), + ), + interaction_metadata: Schema.optional(Schema.Unknown), + message_snapshots: Schema.optional( + Schema.Array( + Schema.Struct({ + message: Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional( + Schema.NullOr(Schema.String), + ), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional( + Schema.Unknown, + ), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional( + Schema.Array(Schema.String), + ), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional( + Schema.Boolean, + ), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional( + Schema.NullOr(Schema.String), + ), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional( + Schema.Unknown, + ), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + }), + }), + ), + ), + reactions: Schema.optional( + Schema.Array( + Schema.Struct({ + emoji: Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + count: Schema.Number, + count_details: Schema.Struct({ + burst: Schema.Number, + normal: Schema.Number, + }), + burst_colors: Schema.Array(Schema.String), + me_burst: Schema.Boolean, + me: Schema.Boolean, + }), + ), + ), + referenced_message: Schema.optional(Schema.Unknown), + }), + ), + ), + }); +export type ListPublicArchivedThreadsOutput = + typeof ListPublicArchivedThreadsOutput.Type; + +// The operation +export const listPublicArchivedThreads = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: ListPublicArchivedThreadsInput, + outputSchema: ListPublicArchivedThreadsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/listStickerPacks.ts b/packages/discord/src/operations/listStickerPacks.ts new file mode 100644 index 000000000..45944a77c --- /dev/null +++ b/packages/discord/src/operations/listStickerPacks.ts @@ -0,0 +1,46 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListStickerPacksInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + {}, +).pipe(T.Http({ method: "GET", path: "/sticker-packs" })); +export type ListStickerPacksInput = typeof ListStickerPacksInput.Type; + +// Output Schema +export const ListStickerPacksOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + sticker_packs: Schema.Array( + Schema.Struct({ + id: Schema.String, + sku_id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + stickers: Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + tags: Schema.String, + type: Schema.Unknown, + format_type: Schema.Unknown, + description: Schema.NullOr(Schema.String), + pack_id: Schema.String, + sort_value: Schema.Number, + }), + ), + cover_sticker_id: Schema.optional(Schema.String), + banner_asset_id: Schema.optional(Schema.String), + }), + ), + }, +); +export type ListStickerPacksOutput = typeof ListStickerPacksOutput.Type; + +// The operation +export const listStickerPacks = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListStickerPacksInput, + outputSchema: ListStickerPacksOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/listThreadMembers.ts b/packages/discord/src/operations/listThreadMembers.ts new file mode 100644 index 000000000..2de2d9d83 --- /dev/null +++ b/packages/discord/src/operations/listThreadMembers.ts @@ -0,0 +1,68 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListThreadMembersInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + channel_id: Schema.String.pipe(T.PathParam()), + with_member: Schema.optional(Schema.Boolean), + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.String), + }, +).pipe( + T.Http({ method: "GET", path: "/channels/{channel_id}/thread-members" }), +); +export type ListThreadMembersInput = typeof ListThreadMembersInput.Type; + +// Output Schema +export const ListThreadMembersOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), +); +export type ListThreadMembersOutput = typeof ListThreadMembersOutput.Type; + +// The operation +export const listThreadMembers = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListThreadMembersInput, + outputSchema: ListThreadMembersOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/listVoiceRegions.ts b/packages/discord/src/operations/listVoiceRegions.ts new file mode 100644 index 000000000..1f3bdbb5d --- /dev/null +++ b/packages/discord/src/operations/listVoiceRegions.ts @@ -0,0 +1,29 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ListVoiceRegionsInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + {}, +).pipe(T.Http({ method: "GET", path: "/voice/regions" })); +export type ListVoiceRegionsInput = typeof ListVoiceRegionsInput.Type; + +// Output Schema +export const ListVoiceRegionsOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + custom: Schema.Boolean, + deprecated: Schema.Boolean, + optimal: Schema.Boolean, + }), +); +export type ListVoiceRegionsOutput = typeof ListVoiceRegionsOutput.Type; + +// The operation +export const listVoiceRegions = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListVoiceRegionsInput, + outputSchema: ListVoiceRegionsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/partnerSdkToken.ts b/packages/discord/src/operations/partnerSdkToken.ts new file mode 100644 index 000000000..5776195c8 --- /dev/null +++ b/packages/discord/src/operations/partnerSdkToken.ts @@ -0,0 +1,34 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; +import { SensitiveString, SensitiveNullableString } from "../sensitive.ts"; + +// Input Schema +export const PartnerSdkTokenInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + client_id: Schema.String, + client_secret: Schema.optional(SensitiveNullableString), + external_auth_token: Schema.String, + external_auth_type: Schema.Unknown, +}).pipe(T.Http({ method: "POST", path: "/partner-sdk/token" })); +export type PartnerSdkTokenInput = typeof PartnerSdkTokenInput.Type; + +// Output Schema +export const PartnerSdkTokenOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + token_type: Schema.String, + access_token: SensitiveString, + expires_in: Schema.Number, + scope: Schema.String, + id_token: Schema.String, + refresh_token: Schema.optional(SensitiveNullableString), + scopes: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + expires_at_s: Schema.optional(Schema.NullOr(Schema.Number)), +}); +export type PartnerSdkTokenOutput = typeof PartnerSdkTokenOutput.Type; + +// The operation +export const partnerSdkToken = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: PartnerSdkTokenInput, + outputSchema: PartnerSdkTokenOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/partnerSdkUnmergeProvisionalAccount.ts b/packages/discord/src/operations/partnerSdkUnmergeProvisionalAccount.ts new file mode 100644 index 000000000..b0da0a02d --- /dev/null +++ b/packages/discord/src/operations/partnerSdkUnmergeProvisionalAccount.ts @@ -0,0 +1,35 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; +import { SensitiveNullableString } from "../sensitive.ts"; + +// Input Schema +export const PartnerSdkUnmergeProvisionalAccountInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + client_id: Schema.String, + client_secret: Schema.optional(SensitiveNullableString), + external_auth_token: Schema.String, + external_auth_type: Schema.Unknown, + }).pipe( + T.Http({ + method: "POST", + path: "/partner-sdk/provisional-accounts/unmerge", + }), + ); +export type PartnerSdkUnmergeProvisionalAccountInput = + typeof PartnerSdkUnmergeProvisionalAccountInput.Type; + +// Output Schema +export const PartnerSdkUnmergeProvisionalAccountOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type PartnerSdkUnmergeProvisionalAccountOutput = + typeof PartnerSdkUnmergeProvisionalAccountOutput.Type; + +// The operation +export const partnerSdkUnmergeProvisionalAccount = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: PartnerSdkUnmergeProvisionalAccountInput, + outputSchema: PartnerSdkUnmergeProvisionalAccountOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/pollExpire.ts b/packages/discord/src/operations/pollExpire.ts new file mode 100644 index 000000000..07bb91c1c --- /dev/null +++ b/packages/discord/src/operations/pollExpire.ts @@ -0,0 +1,854 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const PollExpireInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), +}).pipe( + T.Http({ + method: "POST", + path: "/channels/{channel_id}/polls/{message_id}/expire", + }), +); +export type PollExpireInput = typeof PollExpireInput.Type; + +// Output Schema +export const PollExpireOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + id: Schema.String, + channel_id: Schema.String, + author: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + pinned: Schema.Boolean, + mention_everyone: Schema.Boolean, + tts: Schema.Boolean, + call: Schema.optional( + Schema.Struct({ + ended_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + participants: Schema.Array(Schema.String), + }), + ), + activity: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + party_id: Schema.optional(Schema.String), + }), + ), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + application_id: Schema.optional(Schema.String), + interaction: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + name_localized: Schema.optional(Schema.String), + }), + ), + nonce: Schema.optional(Schema.Unknown), + webhook_id: Schema.optional(Schema.String), + message_reference: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + channel_id: Schema.String, + message_id: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + }), + ), + thread: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + mention_channels: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + type: Schema.Unknown, + guild_id: Schema.String, + }), + ), + ), + role_subscription_data: Schema.optional( + Schema.Struct({ + role_subscription_listing_id: Schema.String, + tier_name: Schema.String, + total_months_subscribed: Schema.Number, + is_renewal: Schema.Boolean, + }), + ), + purchase_notification: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + guild_product_purchase: Schema.optional( + Schema.Struct({ + listing_id: Schema.String, + product_name: Schema.String, + }), + ), + }), + ), + position: Schema.optional(Schema.Number), + resolved: Schema.optional( + Schema.Struct({ + users: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + ), + members: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + channels: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + ), + roles: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ), + ), + ), + }), + ), + poll: Schema.optional( + Schema.Struct({ + question: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + answers: Schema.Array( + Schema.Struct({ + answer_id: Schema.Number, + poll_media: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + }), + ), + expiry: Schema.String, + allow_multiselect: Schema.Boolean, + layout_type: Schema.Unknown, + results: Schema.Struct({ + answer_counts: Schema.Array( + Schema.Struct({ + id: Schema.Number, + count: Schema.Number, + me_voted: Schema.Boolean, + }), + ), + is_finalized: Schema.Boolean, + }), + }), + ), + shared_client_theme: Schema.optional( + Schema.Struct({ + colors: Schema.Array(Schema.String), + gradient_angle: Schema.Number, + base_mix: Schema.Number, + base_theme: Schema.Unknown, + }), + ), + interaction_metadata: Schema.optional(Schema.Unknown), + message_snapshots: Schema.optional( + Schema.Array( + Schema.Struct({ + message: Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + }), + }), + ), + ), + reactions: Schema.optional( + Schema.Array( + Schema.Struct({ + emoji: Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + count: Schema.Number, + count_details: Schema.Struct({ + burst: Schema.Number, + normal: Schema.Number, + }), + burst_colors: Schema.Array(Schema.String), + me_burst: Schema.Boolean, + me: Schema.Boolean, + }), + ), + ), + referenced_message: Schema.optional(Schema.Unknown), +}); +export type PollExpireOutput = typeof PollExpireOutput.Type; + +// The operation +export const pollExpire = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: PollExpireInput, + outputSchema: PollExpireOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/previewPruneGuild.ts b/packages/discord/src/operations/previewPruneGuild.ts new file mode 100644 index 000000000..38fa671fd --- /dev/null +++ b/packages/discord/src/operations/previewPruneGuild.ts @@ -0,0 +1,28 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const PreviewPruneGuildInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + guild_id: Schema.String.pipe(T.PathParam()), + days: Schema.optional(Schema.Number), + include_roles: Schema.optional(Schema.String), + }, +).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/prune" })); +export type PreviewPruneGuildInput = typeof PreviewPruneGuildInput.Type; + +// Output Schema +export const PreviewPruneGuildOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + pruned: Schema.NullOr(Schema.Number), + }); +export type PreviewPruneGuildOutput = typeof PreviewPruneGuildOutput.Type; + +// The operation +export const previewPruneGuild = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: PreviewPruneGuildInput, + outputSchema: PreviewPruneGuildOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/pruneGuild.ts b/packages/discord/src/operations/pruneGuild.ts new file mode 100644 index 000000000..a4166d302 --- /dev/null +++ b/packages/discord/src/operations/pruneGuild.ts @@ -0,0 +1,26 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const PruneGuildInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + days: Schema.optional(Schema.NullOr(Schema.Number)), + compute_prune_count: Schema.optional(Schema.NullOr(Schema.Boolean)), + include_roles: Schema.optional(Schema.Unknown), +}).pipe(T.Http({ method: "POST", path: "/guilds/{guild_id}/prune" })); +export type PruneGuildInput = typeof PruneGuildInput.Type; + +// Output Schema +export const PruneGuildOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + pruned: Schema.NullOr(Schema.Number), +}); +export type PruneGuildOutput = typeof PruneGuildOutput.Type; + +// The operation +export const pruneGuild = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: PruneGuildInput, + outputSchema: PruneGuildOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/putGuildsOnboarding.ts b/packages/discord/src/operations/putGuildsOnboarding.ts new file mode 100644 index 000000000..ed7bbdf92 --- /dev/null +++ b/packages/discord/src/operations/putGuildsOnboarding.ts @@ -0,0 +1,87 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const PutGuildsOnboardingInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + prompts: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + title: Schema.String, + options: Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.Unknown), + title: Schema.String, + description: Schema.optional(Schema.NullOr(Schema.String)), + emoji_id: Schema.optional(Schema.Unknown), + emoji_name: Schema.optional(Schema.NullOr(Schema.String)), + emoji_animated: Schema.optional(Schema.NullOr(Schema.Boolean)), + role_ids: Schema.optional( + Schema.NullOr(Schema.Array(Schema.String)), + ), + channel_ids: Schema.optional( + Schema.NullOr(Schema.Array(Schema.String)), + ), + }), + ), + single_select: Schema.optional(Schema.NullOr(Schema.Boolean)), + required: Schema.optional(Schema.NullOr(Schema.Boolean)), + in_onboarding: Schema.optional(Schema.NullOr(Schema.Boolean)), + type: Schema.optional(Schema.Unknown), + id: Schema.String, + }), + ), + ), + ), + enabled: Schema.optional(Schema.NullOr(Schema.Boolean)), + default_channel_ids: Schema.optional( + Schema.NullOr(Schema.Array(Schema.String)), + ), + mode: Schema.optional(Schema.Unknown), + }).pipe(T.Http({ method: "PUT", path: "/guilds/{guild_id}/onboarding" })); +export type PutGuildsOnboardingInput = typeof PutGuildsOnboardingInput.Type; + +// Output Schema +export const PutGuildsOnboardingOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String, + prompts: Schema.Array( + Schema.Struct({ + id: Schema.String, + title: Schema.String, + options: Schema.Array( + Schema.Struct({ + id: Schema.String, + title: Schema.String, + description: Schema.String, + emoji: Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.Boolean, + }), + role_ids: Schema.Array(Schema.String), + channel_ids: Schema.Array(Schema.String), + }), + ), + single_select: Schema.Boolean, + required: Schema.Boolean, + in_onboarding: Schema.Boolean, + type: Schema.Unknown, + }), + ), + default_channel_ids: Schema.Array(Schema.String), + enabled: Schema.Boolean, + mode: Schema.Unknown, + }); +export type PutGuildsOnboardingOutput = typeof PutGuildsOnboardingOutput.Type; + +// The operation +export const putGuildsOnboarding = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: PutGuildsOnboardingInput, + outputSchema: PutGuildsOnboardingOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/searchGuildMembers.ts b/packages/discord/src/operations/searchGuildMembers.ts new file mode 100644 index 000000000..889f1c563 --- /dev/null +++ b/packages/discord/src/operations/searchGuildMembers.ts @@ -0,0 +1,57 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const SearchGuildMembersInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + limit: Schema.optional(Schema.Number), + query: Schema.String, + }).pipe(T.Http({ method: "GET", path: "/guilds/{guild_id}/members/search" })); +export type SearchGuildMembersInput = typeof SearchGuildMembersInput.Type; + +// Output Schema +export const SearchGuildMembersOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ); +export type SearchGuildMembersOutput = typeof SearchGuildMembersOutput.Type; + +// The operation +export const searchGuildMembers = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: SearchGuildMembersInput, + outputSchema: SearchGuildMembersOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/sendSoundboardSound.ts b/packages/discord/src/operations/sendSoundboardSound.ts new file mode 100644 index 000000000..66a07d6ad --- /dev/null +++ b/packages/discord/src/operations/sendSoundboardSound.ts @@ -0,0 +1,30 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const SendSoundboardSoundInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + sound_id: Schema.String, + source_guild_id: Schema.optional(Schema.Unknown), + }).pipe( + T.Http({ + method: "POST", + path: "/channels/{channel_id}/send-soundboard-sound", + }), + ); +export type SendSoundboardSoundInput = typeof SendSoundboardSoundInput.Type; + +// Output Schema +export const SendSoundboardSoundOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type SendSoundboardSoundOutput = typeof SendSoundboardSoundOutput.Type; + +// The operation +export const sendSoundboardSound = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: SendSoundboardSoundInput, + outputSchema: SendSoundboardSoundOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/setChannelPermissionOverwrite.ts b/packages/discord/src/operations/setChannelPermissionOverwrite.ts new file mode 100644 index 000000000..c34b4e301 --- /dev/null +++ b/packages/discord/src/operations/setChannelPermissionOverwrite.ts @@ -0,0 +1,35 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const SetChannelPermissionOverwriteInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + overwrite_id: Schema.String.pipe(T.PathParam()), + type: Schema.optional(Schema.Unknown), + allow: Schema.optional(Schema.NullOr(Schema.Number)), + deny: Schema.optional(Schema.NullOr(Schema.Number)), + }).pipe( + T.Http({ + method: "PUT", + path: "/channels/{channel_id}/permissions/{overwrite_id}", + }), + ); +export type SetChannelPermissionOverwriteInput = + typeof SetChannelPermissionOverwriteInput.Type; + +// Output Schema +export const SetChannelPermissionOverwriteOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type SetChannelPermissionOverwriteOutput = + typeof SetChannelPermissionOverwriteOutput.Type; + +// The operation +export const setChannelPermissionOverwrite = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: SetChannelPermissionOverwriteInput, + outputSchema: SetChannelPermissionOverwriteOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/setGuildApplicationCommandPermissions.ts b/packages/discord/src/operations/setGuildApplicationCommandPermissions.ts new file mode 100644 index 000000000..f55a0fc03 --- /dev/null +++ b/packages/discord/src/operations/setGuildApplicationCommandPermissions.ts @@ -0,0 +1,55 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const SetGuildApplicationCommandPermissionsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + guild_id: Schema.String.pipe(T.PathParam()), + command_id: Schema.String.pipe(T.PathParam()), + permissions: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + permission: Schema.Boolean, + }), + ), + ), + ), + }).pipe( + T.Http({ + method: "PUT", + path: "/applications/{application_id}/guilds/{guild_id}/commands/{command_id}/permissions", + }), + ); +export type SetGuildApplicationCommandPermissionsInput = + typeof SetGuildApplicationCommandPermissionsInput.Type; + +// Output Schema +export const SetGuildApplicationCommandPermissionsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + application_id: Schema.String, + guild_id: Schema.String, + permissions: Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + permission: Schema.Boolean, + }), + ), + }); +export type SetGuildApplicationCommandPermissionsOutput = + typeof SetGuildApplicationCommandPermissionsOutput.Type; + +// The operation +export const setGuildApplicationCommandPermissions = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: SetGuildApplicationCommandPermissionsInput, + outputSchema: SetGuildApplicationCommandPermissionsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/syncGuildTemplate.ts b/packages/discord/src/operations/syncGuildTemplate.ts new file mode 100644 index 000000000..3ae7f6372 --- /dev/null +++ b/packages/discord/src/operations/syncGuildTemplate.ts @@ -0,0 +1,104 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const SyncGuildTemplateInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + guild_id: Schema.String.pipe(T.PathParam()), + code: Schema.String.pipe(T.PathParam()), + }, +).pipe(T.Http({ method: "PUT", path: "/guilds/{guild_id}/templates/{code}" })); +export type SyncGuildTemplateInput = typeof SyncGuildTemplateInput.Type; + +// Output Schema +export const SyncGuildTemplateOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + code: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + usage_count: Schema.Number, + creator_id: Schema.String, + creator: Schema.Unknown, + created_at: Schema.String, + updated_at: Schema.String, + source_guild_id: Schema.String, + serialized_source_guild: Schema.Struct({ + name: Schema.String, + description: Schema.NullOr(Schema.String), + region: Schema.NullOr(Schema.String), + verification_level: Schema.Unknown, + default_message_notifications: Schema.Unknown, + explicit_content_filter: Schema.Unknown, + preferred_locale: Schema.Unknown, + afk_channel_id: Schema.Unknown, + afk_timeout: Schema.Unknown, + system_channel_id: Schema.Unknown, + system_channel_flags: Schema.Number, + roles: Schema.Array( + Schema.Struct({ + id: Schema.Number, + name: Schema.String, + permissions: Schema.String, + color: Schema.Number, + colors: Schema.Unknown, + hoist: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + }), + ), + channels: Schema.Array( + Schema.Struct({ + id: Schema.NullOr(Schema.Number), + type: Schema.Unknown, + name: Schema.NullOr(Schema.String), + position: Schema.NullOr(Schema.Number), + topic: Schema.NullOr(Schema.String), + bitrate: Schema.Number, + user_limit: Schema.Number, + nsfw: Schema.Boolean, + rate_limit_per_user: Schema.Number, + parent_id: Schema.Unknown, + default_auto_archive_duration: Schema.Unknown, + permission_overwrites: Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + allow: Schema.String, + deny: Schema.String, + }), + ), + available_tags: Schema.NullOr( + Schema.Array( + Schema.Struct({ + id: Schema.NullOr(Schema.Number), + name: Schema.String, + emoji_id: Schema.Unknown, + emoji_name: Schema.NullOr(Schema.String), + moderated: Schema.NullOr(Schema.Boolean), + }), + ), + ), + template: Schema.String, + default_reaction_emoji: Schema.Unknown, + default_thread_rate_limit_per_user: Schema.NullOr(Schema.Number), + default_sort_order: Schema.Unknown, + default_forum_layout: Schema.Unknown, + default_tag_setting: Schema.Unknown, + icon_emoji: Schema.Unknown, + theme_color: Schema.NullOr(Schema.Number), + }), + ), + }), + is_dirty: Schema.NullOr(Schema.Boolean), + }); +export type SyncGuildTemplateOutput = typeof SyncGuildTemplateOutput.Type; + +// The operation +export const syncGuildTemplate = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: SyncGuildTemplateInput, + outputSchema: SyncGuildTemplateOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/threadSearch.ts b/packages/discord/src/operations/threadSearch.ts new file mode 100644 index 000000000..6154a9403 --- /dev/null +++ b/packages/discord/src/operations/threadSearch.ts @@ -0,0 +1,994 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const ThreadSearchInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + name: Schema.optional(Schema.String), + slop: Schema.optional(Schema.Number), + min_id: Schema.optional(Schema.String), + max_id: Schema.optional(Schema.String), + tag: Schema.optional(Schema.String), + tag_setting: Schema.optional(Schema.String), + archived: Schema.optional(Schema.Boolean), + sort_by: Schema.optional(Schema.String), + sort_order: Schema.optional(Schema.String), + limit: Schema.optional(Schema.Number), + offset: Schema.optional(Schema.Number), +}).pipe( + T.Http({ method: "GET", path: "/channels/{channel_id}/threads/search" }), +); +export type ThreadSearchInput = typeof ThreadSearchInput.Type; + +// Output Schema +export const ThreadSearchOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + threads: Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + members: Schema.Array( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + has_more: Schema.Boolean, + first_messages: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + id: Schema.String, + channel_id: Schema.String, + author: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + pinned: Schema.Boolean, + mention_everyone: Schema.Boolean, + tts: Schema.Boolean, + call: Schema.optional( + Schema.Struct({ + ended_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + participants: Schema.Array(Schema.String), + }), + ), + activity: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + party_id: Schema.optional(Schema.String), + }), + ), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + application_id: Schema.optional(Schema.String), + interaction: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + name_localized: Schema.optional(Schema.String), + }), + ), + nonce: Schema.optional(Schema.Unknown), + webhook_id: Schema.optional(Schema.String), + message_reference: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + channel_id: Schema.String, + message_id: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + }), + ), + thread: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + mention_channels: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + type: Schema.Unknown, + guild_id: Schema.String, + }), + ), + ), + role_subscription_data: Schema.optional( + Schema.Struct({ + role_subscription_listing_id: Schema.String, + tier_name: Schema.String, + total_months_subscribed: Schema.Number, + is_renewal: Schema.Boolean, + }), + ), + purchase_notification: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + guild_product_purchase: Schema.optional( + Schema.Struct({ + listing_id: Schema.String, + product_name: Schema.String, + }), + ), + }), + ), + position: Schema.optional(Schema.Number), + resolved: Schema.optional( + Schema.Struct({ + users: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + ), + members: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + channels: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + ), + roles: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ), + ), + ), + }), + ), + poll: Schema.optional( + Schema.Struct({ + question: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + answers: Schema.Array( + Schema.Struct({ + answer_id: Schema.Number, + poll_media: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + }), + ), + expiry: Schema.String, + allow_multiselect: Schema.Boolean, + layout_type: Schema.Unknown, + results: Schema.Struct({ + answer_counts: Schema.Array( + Schema.Struct({ + id: Schema.Number, + count: Schema.Number, + me_voted: Schema.Boolean, + }), + ), + is_finalized: Schema.Boolean, + }), + }), + ), + shared_client_theme: Schema.optional( + Schema.Struct({ + colors: Schema.Array(Schema.String), + gradient_angle: Schema.Number, + base_mix: Schema.Number, + base_theme: Schema.Unknown, + }), + ), + interaction_metadata: Schema.optional(Schema.Unknown), + message_snapshots: Schema.optional( + Schema.Array( + Schema.Struct({ + message: Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional( + Schema.NullOr(Schema.String), + ), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional( + Schema.Unknown, + ), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional( + Schema.Array(Schema.String), + ), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional( + Schema.Unknown, + ), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + }), + }), + ), + ), + reactions: Schema.optional( + Schema.Array( + Schema.Struct({ + emoji: Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + count: Schema.Number, + count_details: Schema.Struct({ + burst: Schema.Number, + normal: Schema.Number, + }), + burst_colors: Schema.Array(Schema.String), + me_burst: Schema.Boolean, + me: Schema.Boolean, + }), + ), + ), + referenced_message: Schema.optional(Schema.Unknown), + }), + ), + ), + total_results: Schema.Number, +}); +export type ThreadSearchOutput = typeof ThreadSearchOutput.Type; + +// The operation +export const threadSearch = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ThreadSearchInput, + outputSchema: ThreadSearchOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/triggerTypingIndicator.ts b/packages/discord/src/operations/triggerTypingIndicator.ts new file mode 100644 index 000000000..cc8050bd8 --- /dev/null +++ b/packages/discord/src/operations/triggerTypingIndicator.ts @@ -0,0 +1,27 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const TriggerTypingIndicatorInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + }).pipe(T.Http({ method: "POST", path: "/channels/{channel_id}/typing" })); +export type TriggerTypingIndicatorInput = + typeof TriggerTypingIndicatorInput.Type; + +// Output Schema +export const TriggerTypingIndicatorOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({}); +export type TriggerTypingIndicatorOutput = + typeof TriggerTypingIndicatorOutput.Type; + +// The operation +export const triggerTypingIndicator = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: TriggerTypingIndicatorInput, + outputSchema: TriggerTypingIndicatorOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/unbanUserFromGuild.ts b/packages/discord/src/operations/unbanUserFromGuild.ts new file mode 100644 index 000000000..31b17cfd0 --- /dev/null +++ b/packages/discord/src/operations/unbanUserFromGuild.ts @@ -0,0 +1,25 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UnbanUserFromGuildInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + user_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ method: "DELETE", path: "/guilds/{guild_id}/bans/{user_id}" }), + ); +export type UnbanUserFromGuildInput = typeof UnbanUserFromGuildInput.Type; + +// Output Schema +export const UnbanUserFromGuildOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type UnbanUserFromGuildOutput = typeof UnbanUserFromGuildOutput.Type; + +// The operation +export const unbanUserFromGuild = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UnbanUserFromGuildInput, + outputSchema: UnbanUserFromGuildOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/updateApplication.ts b/packages/discord/src/operations/updateApplication.ts new file mode 100644 index 000000000..441c794ef --- /dev/null +++ b/packages/discord/src/operations/updateApplication.ts @@ -0,0 +1,141 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateApplicationInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + application_id: Schema.String.pipe(T.PathParam()), + description: Schema.optional( + Schema.NullOr( + Schema.Struct({ + default: Schema.String, + localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + }), + ), + ), + icon: Schema.optional(Schema.NullOr(Schema.String)), + cover_image: Schema.optional(Schema.NullOr(Schema.String)), + team_id: Schema.optional(Schema.Unknown), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + interactions_endpoint_url: Schema.optional(Schema.NullOr(Schema.String)), + explicit_content_filter: Schema.optional(Schema.Unknown), + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + type: Schema.optional(Schema.Unknown), + tags: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + custom_install_url: Schema.optional(Schema.NullOr(Schema.String)), + install_params: Schema.optional(Schema.Unknown), + role_connections_verification_url: Schema.optional( + Schema.NullOr(Schema.String), + ), + integration_types_config: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + ), + event_webhooks_status: Schema.optional(Schema.Unknown), + event_webhooks_url: Schema.optional(Schema.NullOr(Schema.String)), + event_webhooks_types: Schema.optional( + Schema.NullOr(Schema.Array(Schema.Unknown)), + ), + }, +).pipe(T.Http({ method: "PATCH", path: "/applications/{application_id}" })); +export type UpdateApplicationInput = typeof UpdateApplicationInput.Type; + +// Output Schema +export const UpdateApplicationOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + redirect_uris: Schema.Array(Schema.String), + interactions_endpoint_url: Schema.NullOr(Schema.String), + role_connections_verification_url: Schema.NullOr(Schema.String), + owner: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + approximate_guild_count: Schema.Number, + approximate_user_install_count: Schema.Number, + approximate_user_authorization_count: Schema.Number, + event_webhooks_url: Schema.optional(Schema.NullOr(Schema.String)), + event_webhooks_status: Schema.optional(Schema.Unknown), + event_webhooks_types: Schema.optional(Schema.Array(Schema.Unknown)), + explicit_content_filter: Schema.Unknown, + team: Schema.Unknown, + }); +export type UpdateApplicationOutput = typeof UpdateApplicationOutput.Type; + +// The operation +export const updateApplication = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateApplicationInput, + outputSchema: UpdateApplicationOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/updateApplicationCommand.ts b/packages/discord/src/operations/updateApplicationCommand.ts new file mode 100644 index 000000000..10d54e551 --- /dev/null +++ b/packages/discord/src/operations/updateApplicationCommand.ts @@ -0,0 +1,71 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateApplicationCommandInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + command_id: Schema.String.pipe(T.PathParam()), + name: Schema.optional(Schema.String), + name_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + description: Schema.optional(Schema.NullOr(Schema.String)), + description_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + options: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + default_member_permissions: Schema.optional(Schema.NullOr(Schema.Number)), + dm_permission: Schema.optional(Schema.NullOr(Schema.Boolean)), + contexts: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + integration_types: Schema.optional( + Schema.NullOr(Schema.Array(Schema.Unknown)), + ), + handler: Schema.optional(Schema.Unknown), + }).pipe( + T.Http({ + method: "PATCH", + path: "/applications/{application_id}/commands/{command_id}", + }), + ); +export type UpdateApplicationCommandInput = + typeof UpdateApplicationCommandInput.Type; + +// Output Schema +export const UpdateApplicationCommandOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + application_id: Schema.String, + version: Schema.String, + default_member_permissions: Schema.NullOr(Schema.String), + type: Schema.Unknown, + name: Schema.String, + name_localized: Schema.optional(Schema.String), + name_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + description: Schema.String, + description_localized: Schema.optional(Schema.String), + description_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + guild_id: Schema.optional(Schema.String), + dm_permission: Schema.optional(Schema.Boolean), + contexts: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + integration_types: Schema.optional(Schema.Array(Schema.Unknown)), + options: Schema.optional(Schema.Array(Schema.Unknown)), + nsfw: Schema.optional(Schema.Boolean), + }); +export type UpdateApplicationCommandOutput = + typeof UpdateApplicationCommandOutput.Type; + +// The operation +export const updateApplicationCommand = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: UpdateApplicationCommandInput, + outputSchema: UpdateApplicationCommandOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/updateApplicationEmoji.ts b/packages/discord/src/operations/updateApplicationEmoji.ts new file mode 100644 index 000000000..ce9ef5ae4 --- /dev/null +++ b/packages/discord/src/operations/updateApplicationEmoji.ts @@ -0,0 +1,60 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateApplicationEmojiInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + emoji_id: Schema.String.pipe(T.PathParam()), + name: Schema.optional(Schema.String), + }).pipe( + T.Http({ + method: "PATCH", + path: "/applications/{application_id}/emojis/{emoji_id}", + }), + ); +export type UpdateApplicationEmojiInput = + typeof UpdateApplicationEmojiInput.Type; + +// Output Schema +export const UpdateApplicationEmojiOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + roles: Schema.Array(Schema.String), + require_colons: Schema.Boolean, + managed: Schema.Boolean, + animated: Schema.Boolean, + available: Schema.Boolean, + }); +export type UpdateApplicationEmojiOutput = + typeof UpdateApplicationEmojiOutput.Type; + +// The operation +export const updateApplicationEmoji = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: UpdateApplicationEmojiInput, + outputSchema: UpdateApplicationEmojiOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/updateApplicationRoleConnectionsMetadata.ts b/packages/discord/src/operations/updateApplicationRoleConnectionsMetadata.ts new file mode 100644 index 000000000..8ffaecbb6 --- /dev/null +++ b/packages/discord/src/operations/updateApplicationRoleConnectionsMetadata.ts @@ -0,0 +1,44 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateApplicationRoleConnectionsMetadataInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "PUT", + path: "/applications/{application_id}/role-connections/metadata", + }), + ); +export type UpdateApplicationRoleConnectionsMetadataInput = + typeof UpdateApplicationRoleConnectionsMetadataInput.Type; + +// Output Schema +export const UpdateApplicationRoleConnectionsMetadataOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + key: Schema.String, + name: Schema.String, + name_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + description: Schema.String, + description_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + }), + ); +export type UpdateApplicationRoleConnectionsMetadataOutput = + typeof UpdateApplicationRoleConnectionsMetadataOutput.Type; + +// The operation +export const updateApplicationRoleConnectionsMetadata = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateApplicationRoleConnectionsMetadataInput, + outputSchema: UpdateApplicationRoleConnectionsMetadataOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/updateApplicationUserRoleConnection.ts b/packages/discord/src/operations/updateApplicationUserRoleConnection.ts new file mode 100644 index 000000000..70a363388 --- /dev/null +++ b/packages/discord/src/operations/updateApplicationUserRoleConnection.ts @@ -0,0 +1,40 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateApplicationUserRoleConnectionInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + platform_name: Schema.optional(Schema.NullOr(Schema.String)), + platform_username: Schema.optional(Schema.NullOr(Schema.String)), + metadata: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + }).pipe( + T.Http({ + method: "PUT", + path: "/users/@me/applications/{application_id}/role-connection", + }), + ); +export type UpdateApplicationUserRoleConnectionInput = + typeof UpdateApplicationUserRoleConnectionInput.Type; + +// Output Schema +export const UpdateApplicationUserRoleConnectionOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + platform_name: Schema.optional(Schema.String), + platform_username: Schema.optional(Schema.NullOr(Schema.String)), + metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)), + }); +export type UpdateApplicationUserRoleConnectionOutput = + typeof UpdateApplicationUserRoleConnectionOutput.Type; + +// The operation +export const updateApplicationUserRoleConnection = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateApplicationUserRoleConnectionInput, + outputSchema: UpdateApplicationUserRoleConnectionOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/updateAutoModerationRule.ts b/packages/discord/src/operations/updateAutoModerationRule.ts new file mode 100644 index 000000000..01fbb5f8e --- /dev/null +++ b/packages/discord/src/operations/updateAutoModerationRule.ts @@ -0,0 +1,33 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateAutoModerationRuleInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + rule_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "PATCH", + path: "/guilds/{guild_id}/auto-moderation/rules/{rule_id}", + }), + ); +export type UpdateAutoModerationRuleInput = + typeof UpdateAutoModerationRuleInput.Type; + +// Output Schema +export const UpdateAutoModerationRuleOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Unknown; +export type UpdateAutoModerationRuleOutput = + typeof UpdateAutoModerationRuleOutput.Type; + +// The operation +export const updateAutoModerationRule = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: UpdateAutoModerationRuleInput, + outputSchema: UpdateAutoModerationRuleOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/updateChannel.ts b/packages/discord/src/operations/updateChannel.ts new file mode 100644 index 000000000..6e9b045c7 --- /dev/null +++ b/packages/discord/src/operations/updateChannel.ts @@ -0,0 +1,21 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateChannelInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "PATCH", path: "/channels/{channel_id}" })); +export type UpdateChannelInput = typeof UpdateChannelInput.Type; + +// Output Schema +export const UpdateChannelOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Unknown; +export type UpdateChannelOutput = typeof UpdateChannelOutput.Type; + +// The operation +export const updateChannel = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateChannelInput, + outputSchema: UpdateChannelOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/updateGuild.ts b/packages/discord/src/operations/updateGuild.ts new file mode 100644 index 000000000..780704086 --- /dev/null +++ b/packages/discord/src/operations/updateGuild.ts @@ -0,0 +1,175 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateGuildInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + name: Schema.optional(Schema.String), + description: Schema.optional(Schema.NullOr(Schema.String)), + region: Schema.optional(Schema.NullOr(Schema.String)), + icon: Schema.optional(Schema.NullOr(Schema.String)), + verification_level: Schema.optional(Schema.Unknown), + default_message_notifications: Schema.optional(Schema.Unknown), + explicit_content_filter: Schema.optional(Schema.Unknown), + preferred_locale: Schema.optional(Schema.Unknown), + afk_timeout: Schema.optional(Schema.Unknown), + afk_channel_id: Schema.optional(Schema.Unknown), + system_channel_id: Schema.optional(Schema.Unknown), + splash: Schema.optional(Schema.NullOr(Schema.String)), + banner: Schema.optional(Schema.NullOr(Schema.String)), + system_channel_flags: Schema.optional(Schema.NullOr(Schema.Number)), + features: Schema.optional( + Schema.NullOr(Schema.Array(Schema.NullOr(Schema.String))), + ), + discovery_splash: Schema.optional(Schema.NullOr(Schema.String)), + home_header: Schema.optional(Schema.NullOr(Schema.String)), + rules_channel_id: Schema.optional(Schema.Unknown), + safety_alerts_channel_id: Schema.optional(Schema.Unknown), + public_updates_channel_id: Schema.optional(Schema.Unknown), + premium_progress_bar_enabled: Schema.optional(Schema.NullOr(Schema.Boolean)), +}).pipe(T.Http({ method: "PATCH", path: "/guilds/{guild_id}" })); +export type UpdateGuildInput = typeof UpdateGuildInput.Type; + +// Output Schema +export const UpdateGuildOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.NullOr(Schema.String), + home_header: Schema.NullOr(Schema.String), + splash: Schema.NullOr(Schema.String), + discovery_splash: Schema.NullOr(Schema.String), + features: Schema.Array(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + owner_id: Schema.String, + application_id: Schema.Unknown, + region: Schema.String, + afk_channel_id: Schema.Unknown, + afk_timeout: Schema.Unknown, + system_channel_id: Schema.Unknown, + system_channel_flags: Schema.Number, + widget_enabled: Schema.Boolean, + widget_channel_id: Schema.Unknown, + verification_level: Schema.Unknown, + roles: Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ), + default_message_notifications: Schema.Unknown, + mfa_level: Schema.Unknown, + explicit_content_filter: Schema.Unknown, + max_presences: Schema.NullOr(Schema.Number), + max_members: Schema.Number, + max_stage_video_channel_users: Schema.Number, + max_video_channel_users: Schema.Number, + vanity_url_code: Schema.NullOr(Schema.String), + premium_tier: Schema.Unknown, + premium_subscription_count: Schema.Number, + preferred_locale: Schema.Unknown, + rules_channel_id: Schema.Unknown, + safety_alerts_channel_id: Schema.Unknown, + public_updates_channel_id: Schema.Unknown, + premium_progress_bar_enabled: Schema.Boolean, + premium_progress_bar_enabled_user_updated_at: Schema.optional( + Schema.NullOr(Schema.String), + ), + nsfw: Schema.Boolean, + nsfw_level: Schema.Unknown, + emojis: Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + roles: Schema.Array(Schema.String), + require_colons: Schema.Boolean, + managed: Schema.Boolean, + animated: Schema.Boolean, + available: Schema.Boolean, + }), + ), + stickers: Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + tags: Schema.String, + type: Schema.Unknown, + format_type: Schema.Unknown, + description: Schema.NullOr(Schema.String), + available: Schema.Boolean, + guild_id: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + incidents_data: Schema.Unknown, +}); +export type UpdateGuildOutput = typeof UpdateGuildOutput.Type; + +// The operation +export const updateGuild = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateGuildInput, + outputSchema: UpdateGuildOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/updateGuildApplicationCommand.ts b/packages/discord/src/operations/updateGuildApplicationCommand.ts new file mode 100644 index 000000000..31986b926 --- /dev/null +++ b/packages/discord/src/operations/updateGuildApplicationCommand.ts @@ -0,0 +1,71 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateGuildApplicationCommandInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + guild_id: Schema.String.pipe(T.PathParam()), + command_id: Schema.String.pipe(T.PathParam()), + name: Schema.optional(Schema.String), + name_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + description: Schema.optional(Schema.NullOr(Schema.String)), + description_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + options: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + default_member_permissions: Schema.optional(Schema.NullOr(Schema.Number)), + dm_permission: Schema.optional(Schema.NullOr(Schema.Boolean)), + contexts: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + integration_types: Schema.optional( + Schema.NullOr(Schema.Array(Schema.Unknown)), + ), + handler: Schema.optional(Schema.Unknown), + }).pipe( + T.Http({ + method: "PATCH", + path: "/applications/{application_id}/guilds/{guild_id}/commands/{command_id}", + }), + ); +export type UpdateGuildApplicationCommandInput = + typeof UpdateGuildApplicationCommandInput.Type; + +// Output Schema +export const UpdateGuildApplicationCommandOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + application_id: Schema.String, + version: Schema.String, + default_member_permissions: Schema.NullOr(Schema.String), + type: Schema.Unknown, + name: Schema.String, + name_localized: Schema.optional(Schema.String), + name_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + description: Schema.String, + description_localized: Schema.optional(Schema.String), + description_localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + guild_id: Schema.optional(Schema.String), + dm_permission: Schema.optional(Schema.Boolean), + contexts: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + integration_types: Schema.optional(Schema.Array(Schema.Unknown)), + options: Schema.optional(Schema.Array(Schema.Unknown)), + nsfw: Schema.optional(Schema.Boolean), + }); +export type UpdateGuildApplicationCommandOutput = + typeof UpdateGuildApplicationCommandOutput.Type; + +// The operation +export const updateGuildApplicationCommand = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateGuildApplicationCommandInput, + outputSchema: UpdateGuildApplicationCommandOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/updateGuildEmoji.ts b/packages/discord/src/operations/updateGuildEmoji.ts new file mode 100644 index 000000000..93e4d68b0 --- /dev/null +++ b/packages/discord/src/operations/updateGuildEmoji.ts @@ -0,0 +1,54 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateGuildEmojiInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + emoji_id: Schema.String.pipe(T.PathParam()), + name: Schema.optional(Schema.String), + roles: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), +}).pipe( + T.Http({ method: "PATCH", path: "/guilds/{guild_id}/emojis/{emoji_id}" }), +); +export type UpdateGuildEmojiInput = typeof UpdateGuildEmojiInput.Type; + +// Output Schema +export const UpdateGuildEmojiOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + id: Schema.String, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + roles: Schema.Array(Schema.String), + require_colons: Schema.Boolean, + managed: Schema.Boolean, + animated: Schema.Boolean, + available: Schema.Boolean, + }, +); +export type UpdateGuildEmojiOutput = typeof UpdateGuildEmojiOutput.Type; + +// The operation +export const updateGuildEmoji = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateGuildEmojiInput, + outputSchema: UpdateGuildEmojiOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/updateGuildMember.ts b/packages/discord/src/operations/updateGuildMember.ts new file mode 100644 index 000000000..85e20df40 --- /dev/null +++ b/packages/discord/src/operations/updateGuildMember.ts @@ -0,0 +1,64 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateGuildMemberInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + guild_id: Schema.String.pipe(T.PathParam()), + user_id: Schema.String.pipe(T.PathParam()), + nick: Schema.optional(Schema.NullOr(Schema.String)), + roles: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + mute: Schema.optional(Schema.NullOr(Schema.Boolean)), + deaf: Schema.optional(Schema.NullOr(Schema.Boolean)), + channel_id: Schema.optional(Schema.Unknown), + communication_disabled_until: Schema.optional(Schema.NullOr(Schema.String)), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + }, +).pipe( + T.Http({ method: "PATCH", path: "/guilds/{guild_id}/members/{user_id}" }), +); +export type UpdateGuildMemberInput = typeof UpdateGuildMemberInput.Type; + +// Output Schema +export const UpdateGuildMemberOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }); +export type UpdateGuildMemberOutput = typeof UpdateGuildMemberOutput.Type; + +// The operation +export const updateGuildMember = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateGuildMemberInput, + outputSchema: UpdateGuildMemberOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/updateGuildRole.ts b/packages/discord/src/operations/updateGuildRole.ts new file mode 100644 index 000000000..8731e2a6c --- /dev/null +++ b/packages/discord/src/operations/updateGuildRole.ts @@ -0,0 +1,60 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateGuildRoleInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + role_id: Schema.String.pipe(T.PathParam()), + name: Schema.optional(Schema.NullOr(Schema.String)), + permissions: Schema.optional(Schema.NullOr(Schema.Number)), + color: Schema.optional(Schema.NullOr(Schema.Number)), + colors: Schema.optional(Schema.Unknown), + hoist: Schema.optional(Schema.NullOr(Schema.Boolean)), + mentionable: Schema.optional(Schema.NullOr(Schema.Boolean)), + icon: Schema.optional(Schema.NullOr(Schema.String)), + unicode_emoji: Schema.optional(Schema.NullOr(Schema.String)), +}).pipe( + T.Http({ method: "PATCH", path: "/guilds/{guild_id}/roles/{role_id}" }), +); +export type UpdateGuildRoleInput = typeof UpdateGuildRoleInput.Type; + +// Output Schema +export const UpdateGuildRoleOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, +}); +export type UpdateGuildRoleOutput = typeof UpdateGuildRoleOutput.Type; + +// The operation +export const updateGuildRole = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateGuildRoleInput, + outputSchema: UpdateGuildRoleOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/updateGuildScheduledEvent.ts b/packages/discord/src/operations/updateGuildScheduledEvent.ts new file mode 100644 index 000000000..25f404b3b --- /dev/null +++ b/packages/discord/src/operations/updateGuildScheduledEvent.ts @@ -0,0 +1,33 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateGuildScheduledEventInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + guild_scheduled_event_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "PATCH", + path: "/guilds/{guild_id}/scheduled-events/{guild_scheduled_event_id}", + }), + ); +export type UpdateGuildScheduledEventInput = + typeof UpdateGuildScheduledEventInput.Type; + +// Output Schema +export const UpdateGuildScheduledEventOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Unknown; +export type UpdateGuildScheduledEventOutput = + typeof UpdateGuildScheduledEventOutput.Type; + +// The operation +export const updateGuildScheduledEvent = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: UpdateGuildScheduledEventInput, + outputSchema: UpdateGuildScheduledEventOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/updateGuildSoundboardSound.ts b/packages/discord/src/operations/updateGuildSoundboardSound.ts new file mode 100644 index 000000000..07831181c --- /dev/null +++ b/packages/discord/src/operations/updateGuildSoundboardSound.ts @@ -0,0 +1,63 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateGuildSoundboardSoundInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + sound_id: Schema.String.pipe(T.PathParam()), + name: Schema.optional(Schema.String), + volume: Schema.optional(Schema.NullOr(Schema.Number)), + emoji_id: Schema.optional(Schema.Unknown), + emoji_name: Schema.optional(Schema.NullOr(Schema.String)), + }).pipe( + T.Http({ + method: "PATCH", + path: "/guilds/{guild_id}/soundboard-sounds/{sound_id}", + }), + ); +export type UpdateGuildSoundboardSoundInput = + typeof UpdateGuildSoundboardSoundInput.Type; + +// Output Schema +export const UpdateGuildSoundboardSoundOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + name: Schema.String, + sound_id: Schema.String, + volume: Schema.Number, + emoji_id: Schema.Unknown, + emoji_name: Schema.NullOr(Schema.String), + guild_id: Schema.optional(Schema.String), + available: Schema.Boolean, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }); +export type UpdateGuildSoundboardSoundOutput = + typeof UpdateGuildSoundboardSoundOutput.Type; + +// The operation +export const updateGuildSoundboardSound = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: UpdateGuildSoundboardSoundInput, + outputSchema: UpdateGuildSoundboardSoundOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/updateGuildSticker.ts b/packages/discord/src/operations/updateGuildSticker.ts new file mode 100644 index 000000000..7d53b619e --- /dev/null +++ b/packages/discord/src/operations/updateGuildSticker.ts @@ -0,0 +1,59 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateGuildStickerInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + sticker_id: Schema.String.pipe(T.PathParam()), + name: Schema.optional(Schema.String), + tags: Schema.optional(Schema.String), + description: Schema.optional(Schema.NullOr(Schema.String)), + }).pipe( + T.Http({ + method: "PATCH", + path: "/guilds/{guild_id}/stickers/{sticker_id}", + }), + ); +export type UpdateGuildStickerInput = typeof UpdateGuildStickerInput.Type; + +// Output Schema +export const UpdateGuildStickerOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + name: Schema.String, + tags: Schema.String, + type: Schema.Unknown, + format_type: Schema.Unknown, + description: Schema.NullOr(Schema.String), + available: Schema.Boolean, + guild_id: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }); +export type UpdateGuildStickerOutput = typeof UpdateGuildStickerOutput.Type; + +// The operation +export const updateGuildSticker = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateGuildStickerInput, + outputSchema: UpdateGuildStickerOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/updateGuildTemplate.ts b/packages/discord/src/operations/updateGuildTemplate.ts new file mode 100644 index 000000000..6e5cccb84 --- /dev/null +++ b/packages/discord/src/operations/updateGuildTemplate.ts @@ -0,0 +1,107 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateGuildTemplateInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + code: Schema.String.pipe(T.PathParam()), + name: Schema.optional(Schema.String), + description: Schema.optional(Schema.NullOr(Schema.String)), + }).pipe( + T.Http({ method: "PATCH", path: "/guilds/{guild_id}/templates/{code}" }), + ); +export type UpdateGuildTemplateInput = typeof UpdateGuildTemplateInput.Type; + +// Output Schema +export const UpdateGuildTemplateOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + code: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + usage_count: Schema.Number, + creator_id: Schema.String, + creator: Schema.Unknown, + created_at: Schema.String, + updated_at: Schema.String, + source_guild_id: Schema.String, + serialized_source_guild: Schema.Struct({ + name: Schema.String, + description: Schema.NullOr(Schema.String), + region: Schema.NullOr(Schema.String), + verification_level: Schema.Unknown, + default_message_notifications: Schema.Unknown, + explicit_content_filter: Schema.Unknown, + preferred_locale: Schema.Unknown, + afk_channel_id: Schema.Unknown, + afk_timeout: Schema.Unknown, + system_channel_id: Schema.Unknown, + system_channel_flags: Schema.Number, + roles: Schema.Array( + Schema.Struct({ + id: Schema.Number, + name: Schema.String, + permissions: Schema.String, + color: Schema.Number, + colors: Schema.Unknown, + hoist: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + }), + ), + channels: Schema.Array( + Schema.Struct({ + id: Schema.NullOr(Schema.Number), + type: Schema.Unknown, + name: Schema.NullOr(Schema.String), + position: Schema.NullOr(Schema.Number), + topic: Schema.NullOr(Schema.String), + bitrate: Schema.Number, + user_limit: Schema.Number, + nsfw: Schema.Boolean, + rate_limit_per_user: Schema.Number, + parent_id: Schema.Unknown, + default_auto_archive_duration: Schema.Unknown, + permission_overwrites: Schema.Array( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + allow: Schema.String, + deny: Schema.String, + }), + ), + available_tags: Schema.NullOr( + Schema.Array( + Schema.Struct({ + id: Schema.NullOr(Schema.Number), + name: Schema.String, + emoji_id: Schema.Unknown, + emoji_name: Schema.NullOr(Schema.String), + moderated: Schema.NullOr(Schema.Boolean), + }), + ), + ), + template: Schema.String, + default_reaction_emoji: Schema.Unknown, + default_thread_rate_limit_per_user: Schema.NullOr(Schema.Number), + default_sort_order: Schema.Unknown, + default_forum_layout: Schema.Unknown, + default_tag_setting: Schema.Unknown, + icon_emoji: Schema.Unknown, + theme_color: Schema.NullOr(Schema.Number), + }), + ), + }), + is_dirty: Schema.NullOr(Schema.Boolean), + }); +export type UpdateGuildTemplateOutput = typeof UpdateGuildTemplateOutput.Type; + +// The operation +export const updateGuildTemplate = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateGuildTemplateInput, + outputSchema: UpdateGuildTemplateOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/updateGuildWelcomeScreen.ts b/packages/discord/src/operations/updateGuildWelcomeScreen.ts new file mode 100644 index 000000000..3ace1cc15 --- /dev/null +++ b/packages/discord/src/operations/updateGuildWelcomeScreen.ts @@ -0,0 +1,53 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateGuildWelcomeScreenInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + description: Schema.optional(Schema.NullOr(Schema.String)), + welcome_channels: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + channel_id: Schema.String, + description: Schema.String, + emoji_id: Schema.optional(Schema.Unknown), + emoji_name: Schema.optional(Schema.NullOr(Schema.String)), + }), + ), + ), + ), + enabled: Schema.optional(Schema.NullOr(Schema.Boolean)), + }).pipe( + T.Http({ method: "PATCH", path: "/guilds/{guild_id}/welcome-screen" }), + ); +export type UpdateGuildWelcomeScreenInput = + typeof UpdateGuildWelcomeScreenInput.Type; + +// Output Schema +export const UpdateGuildWelcomeScreenOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + description: Schema.NullOr(Schema.String), + welcome_channels: Schema.Array( + Schema.Struct({ + channel_id: Schema.String, + description: Schema.String, + emoji_id: Schema.Unknown, + emoji_name: Schema.NullOr(Schema.String), + }), + ), + }); +export type UpdateGuildWelcomeScreenOutput = + typeof UpdateGuildWelcomeScreenOutput.Type; + +// The operation +export const updateGuildWelcomeScreen = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: UpdateGuildWelcomeScreenInput, + outputSchema: UpdateGuildWelcomeScreenOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/updateGuildWidgetSettings.ts b/packages/discord/src/operations/updateGuildWidgetSettings.ts new file mode 100644 index 000000000..0d412964f --- /dev/null +++ b/packages/discord/src/operations/updateGuildWidgetSettings.ts @@ -0,0 +1,32 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateGuildWidgetSettingsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + channel_id: Schema.optional(Schema.Unknown), + enabled: Schema.optional(Schema.NullOr(Schema.Boolean)), + }).pipe(T.Http({ method: "PATCH", path: "/guilds/{guild_id}/widget" })); +export type UpdateGuildWidgetSettingsInput = + typeof UpdateGuildWidgetSettingsInput.Type; + +// Output Schema +export const UpdateGuildWidgetSettingsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + enabled: Schema.Boolean, + channel_id: Schema.Unknown, + }); +export type UpdateGuildWidgetSettingsOutput = + typeof UpdateGuildWidgetSettingsOutput.Type; + +// The operation +export const updateGuildWidgetSettings = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: UpdateGuildWidgetSettingsInput, + outputSchema: UpdateGuildWidgetSettingsOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/updateInviteTargetUsers.ts b/packages/discord/src/operations/updateInviteTargetUsers.ts new file mode 100644 index 000000000..816c930cc --- /dev/null +++ b/packages/discord/src/operations/updateInviteTargetUsers.ts @@ -0,0 +1,37 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateInviteTargetUsersInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + code: Schema.String.pipe(T.PathParam()), + target_users_file: Schema.String, + }).pipe( + T.Http({ + method: "PUT", + path: "/invites/{code}/target-users", + contentType: "multipart", + }), + ); +export type UpdateInviteTargetUsersInput = + typeof UpdateInviteTargetUsersInput.Type; + +// Output Schema +export const UpdateInviteTargetUsersOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type UpdateInviteTargetUsersOutput = + typeof UpdateInviteTargetUsersOutput.Type; + +// The operation +/** + * Update the target users for an existing invite. + */ +export const updateInviteTargetUsers = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: UpdateInviteTargetUsersInput, + outputSchema: UpdateInviteTargetUsersOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/updateLobbyMessageExternalModerationMetadata.ts b/packages/discord/src/operations/updateLobbyMessageExternalModerationMetadata.ts new file mode 100644 index 000000000..9912f43b2 --- /dev/null +++ b/packages/discord/src/operations/updateLobbyMessageExternalModerationMetadata.ts @@ -0,0 +1,35 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateLobbyMessageExternalModerationMetadataInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + lobby_id: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "PUT", + path: "/lobbies/{lobby_id}/messages/{message_id}/moderation-metadata", + }), + ); +export type UpdateLobbyMessageExternalModerationMetadataInput = + typeof UpdateLobbyMessageExternalModerationMetadataInput.Type; + +// Output Schema +export const UpdateLobbyMessageExternalModerationMetadataOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type UpdateLobbyMessageExternalModerationMetadataOutput = + typeof UpdateLobbyMessageExternalModerationMetadataOutput.Type; + +// The operation +/** + * Update the external moderation metadata for a lobby message. + */ +export const updateLobbyMessageExternalModerationMetadata = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateLobbyMessageExternalModerationMetadataInput, + outputSchema: UpdateLobbyMessageExternalModerationMetadataOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/updateMessage.ts b/packages/discord/src/operations/updateMessage.ts new file mode 100644 index 000000000..5b4241fe9 --- /dev/null +++ b/packages/discord/src/operations/updateMessage.ts @@ -0,0 +1,905 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateMessageInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), + content: Schema.optional(Schema.NullOr(Schema.String)), + embeds: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + type: Schema.optional(Schema.NullOr(Schema.String)), + url: Schema.optional(Schema.NullOr(Schema.String)), + title: Schema.optional(Schema.NullOr(Schema.String)), + color: Schema.optional(Schema.NullOr(Schema.Number)), + timestamp: Schema.optional(Schema.NullOr(Schema.String)), + description: Schema.optional(Schema.NullOr(Schema.String)), + author: Schema.optional(Schema.Unknown), + image: Schema.optional(Schema.Unknown), + thumbnail: Schema.optional(Schema.Unknown), + footer: Schema.optional(Schema.Unknown), + fields: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.optional(Schema.NullOr(Schema.Boolean)), + }), + ), + ), + ), + provider: Schema.optional(Schema.Unknown), + video: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + allowed_mentions: Schema.optional(Schema.Unknown), + sticker_ids: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + components: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + attachments: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.optional(Schema.NullOr(Schema.String)), + description: Schema.optional(Schema.NullOr(Schema.String)), + duration_secs: Schema.optional(Schema.NullOr(Schema.Number)), + waveform: Schema.optional(Schema.NullOr(Schema.String)), + title: Schema.optional(Schema.NullOr(Schema.String)), + is_remix: Schema.optional(Schema.NullOr(Schema.Boolean)), + }), + ), + ), + ), +}).pipe( + T.Http({ + method: "PATCH", + path: "/channels/{channel_id}/messages/{message_id}", + }), +); +export type UpdateMessageInput = typeof UpdateMessageInput.Type; + +// Output Schema +export const UpdateMessageOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + id: Schema.String, + channel_id: Schema.String, + author: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + pinned: Schema.Boolean, + mention_everyone: Schema.Boolean, + tts: Schema.Boolean, + call: Schema.optional( + Schema.Struct({ + ended_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + participants: Schema.Array(Schema.String), + }), + ), + activity: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + party_id: Schema.optional(Schema.String), + }), + ), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + application_id: Schema.optional(Schema.String), + interaction: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + name_localized: Schema.optional(Schema.String), + }), + ), + nonce: Schema.optional(Schema.Unknown), + webhook_id: Schema.optional(Schema.String), + message_reference: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + channel_id: Schema.String, + message_id: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + }), + ), + thread: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + mention_channels: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + type: Schema.Unknown, + guild_id: Schema.String, + }), + ), + ), + role_subscription_data: Schema.optional( + Schema.Struct({ + role_subscription_listing_id: Schema.String, + tier_name: Schema.String, + total_months_subscribed: Schema.Number, + is_renewal: Schema.Boolean, + }), + ), + purchase_notification: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + guild_product_purchase: Schema.optional( + Schema.Struct({ + listing_id: Schema.String, + product_name: Schema.String, + }), + ), + }), + ), + position: Schema.optional(Schema.Number), + resolved: Schema.optional( + Schema.Struct({ + users: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + ), + members: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + channels: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + ), + roles: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ), + ), + ), + }), + ), + poll: Schema.optional( + Schema.Struct({ + question: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + answers: Schema.Array( + Schema.Struct({ + answer_id: Schema.Number, + poll_media: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + }), + ), + expiry: Schema.String, + allow_multiselect: Schema.Boolean, + layout_type: Schema.Unknown, + results: Schema.Struct({ + answer_counts: Schema.Array( + Schema.Struct({ + id: Schema.Number, + count: Schema.Number, + me_voted: Schema.Boolean, + }), + ), + is_finalized: Schema.Boolean, + }), + }), + ), + shared_client_theme: Schema.optional( + Schema.Struct({ + colors: Schema.Array(Schema.String), + gradient_angle: Schema.Number, + base_mix: Schema.Number, + base_theme: Schema.Unknown, + }), + ), + interaction_metadata: Schema.optional(Schema.Unknown), + message_snapshots: Schema.optional( + Schema.Array( + Schema.Struct({ + message: Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + }), + }), + ), + ), + reactions: Schema.optional( + Schema.Array( + Schema.Struct({ + emoji: Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + count: Schema.Number, + count_details: Schema.Struct({ + burst: Schema.Number, + normal: Schema.Number, + }), + burst_colors: Schema.Array(Schema.String), + me_burst: Schema.Boolean, + me: Schema.Boolean, + }), + ), + ), + referenced_message: Schema.optional(Schema.Unknown), +}); +export type UpdateMessageOutput = typeof UpdateMessageOutput.Type; + +// The operation +export const updateMessage = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateMessageInput, + outputSchema: UpdateMessageOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/updateMyApplication.ts b/packages/discord/src/operations/updateMyApplication.ts new file mode 100644 index 000000000..3cf7116f1 --- /dev/null +++ b/packages/discord/src/operations/updateMyApplication.ts @@ -0,0 +1,139 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateMyApplicationInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + description: Schema.optional( + Schema.NullOr( + Schema.Struct({ + default: Schema.String, + localizations: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.String)), + ), + }), + ), + ), + icon: Schema.optional(Schema.NullOr(Schema.String)), + cover_image: Schema.optional(Schema.NullOr(Schema.String)), + team_id: Schema.optional(Schema.Unknown), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + interactions_endpoint_url: Schema.optional(Schema.NullOr(Schema.String)), + explicit_content_filter: Schema.optional(Schema.Unknown), + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + type: Schema.optional(Schema.Unknown), + tags: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + custom_install_url: Schema.optional(Schema.NullOr(Schema.String)), + install_params: Schema.optional(Schema.Unknown), + role_connections_verification_url: Schema.optional( + Schema.NullOr(Schema.String), + ), + integration_types_config: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + ), + event_webhooks_status: Schema.optional(Schema.Unknown), + event_webhooks_url: Schema.optional(Schema.NullOr(Schema.String)), + event_webhooks_types: Schema.optional( + Schema.NullOr(Schema.Array(Schema.Unknown)), + ), + }).pipe(T.Http({ method: "PATCH", path: "/applications/@me" })); +export type UpdateMyApplicationInput = typeof UpdateMyApplicationInput.Type; + +// Output Schema +export const UpdateMyApplicationOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + redirect_uris: Schema.Array(Schema.String), + interactions_endpoint_url: Schema.NullOr(Schema.String), + role_connections_verification_url: Schema.NullOr(Schema.String), + owner: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + approximate_guild_count: Schema.Number, + approximate_user_install_count: Schema.Number, + approximate_user_authorization_count: Schema.Number, + event_webhooks_url: Schema.optional(Schema.NullOr(Schema.String)), + event_webhooks_status: Schema.optional(Schema.Unknown), + event_webhooks_types: Schema.optional(Schema.Array(Schema.Unknown)), + explicit_content_filter: Schema.Unknown, + team: Schema.Unknown, + }); +export type UpdateMyApplicationOutput = typeof UpdateMyApplicationOutput.Type; + +// The operation +export const updateMyApplication = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateMyApplicationInput, + outputSchema: UpdateMyApplicationOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/updateMyGuildMember.ts b/packages/discord/src/operations/updateMyGuildMember.ts new file mode 100644 index 000000000..404125dbc --- /dev/null +++ b/packages/discord/src/operations/updateMyGuildMember.ts @@ -0,0 +1,58 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateMyGuildMemberInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + nick: Schema.optional(Schema.NullOr(Schema.String)), + avatar: Schema.optional(Schema.NullOr(Schema.String)), + bio: Schema.optional(Schema.NullOr(Schema.String)), + banner: Schema.optional(Schema.NullOr(Schema.String)), + }).pipe(T.Http({ method: "PATCH", path: "/guilds/{guild_id}/members/@me" })); +export type UpdateMyGuildMemberInput = typeof UpdateMyGuildMemberInput.Type; + +// Output Schema +export const UpdateMyGuildMemberOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + permissions: Schema.optional(Schema.String), + }); +export type UpdateMyGuildMemberOutput = typeof UpdateMyGuildMemberOutput.Type; + +// The operation +export const updateMyGuildMember = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateMyGuildMemberInput, + outputSchema: UpdateMyGuildMemberOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/updateMyUser.ts b/packages/discord/src/operations/updateMyUser.ts new file mode 100644 index 000000000..02ad19a37 --- /dev/null +++ b/packages/discord/src/operations/updateMyUser.ts @@ -0,0 +1,43 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateMyUserInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + username: Schema.String, + avatar: Schema.optional(Schema.NullOr(Schema.String)), + banner: Schema.optional(Schema.NullOr(Schema.String)), +}).pipe(T.Http({ method: "PATCH", path: "/users/@me" })); +export type UpdateMyUserInput = typeof UpdateMyUserInput.Type; + +// Output Schema +export const UpdateMyUserOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.optional(Schema.Unknown), + mfa_enabled: Schema.Boolean, + locale: Schema.Unknown, + premium_type: Schema.optional(Schema.Unknown), + email: Schema.optional(Schema.NullOr(Schema.String)), + verified: Schema.optional(Schema.Boolean), +}); +export type UpdateMyUserOutput = typeof UpdateMyUserOutput.Type; + +// The operation +export const updateMyUser = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateMyUserInput, + outputSchema: UpdateMyUserOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/updateOriginalWebhookMessage.ts b/packages/discord/src/operations/updateOriginalWebhookMessage.ts new file mode 100644 index 000000000..7514716a6 --- /dev/null +++ b/packages/discord/src/operations/updateOriginalWebhookMessage.ts @@ -0,0 +1,914 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateOriginalWebhookMessageInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + webhook_id: Schema.String.pipe(T.PathParam()), + webhook_token: Schema.String.pipe(T.PathParam()), + thread_id: Schema.optional(Schema.String), + with_components: Schema.optional(Schema.Boolean), + content: Schema.optional(Schema.NullOr(Schema.String)), + embeds: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + type: Schema.optional(Schema.NullOr(Schema.String)), + url: Schema.optional(Schema.NullOr(Schema.String)), + title: Schema.optional(Schema.NullOr(Schema.String)), + color: Schema.optional(Schema.NullOr(Schema.Number)), + timestamp: Schema.optional(Schema.NullOr(Schema.String)), + description: Schema.optional(Schema.NullOr(Schema.String)), + author: Schema.optional(Schema.Unknown), + image: Schema.optional(Schema.Unknown), + thumbnail: Schema.optional(Schema.Unknown), + footer: Schema.optional(Schema.Unknown), + fields: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.optional(Schema.NullOr(Schema.Boolean)), + }), + ), + ), + ), + provider: Schema.optional(Schema.Unknown), + video: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + allowed_mentions: Schema.optional(Schema.Unknown), + components: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + attachments: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.optional(Schema.NullOr(Schema.String)), + description: Schema.optional(Schema.NullOr(Schema.String)), + duration_secs: Schema.optional(Schema.NullOr(Schema.Number)), + waveform: Schema.optional(Schema.NullOr(Schema.String)), + title: Schema.optional(Schema.NullOr(Schema.String)), + is_remix: Schema.optional(Schema.NullOr(Schema.Boolean)), + }), + ), + ), + ), + poll: Schema.optional(Schema.Unknown), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + }).pipe( + T.Http({ + method: "PATCH", + path: "/webhooks/{webhook_id}/{webhook_token}/messages/@original", + }), + ); +export type UpdateOriginalWebhookMessageInput = + typeof UpdateOriginalWebhookMessageInput.Type; + +// Output Schema +export const UpdateOriginalWebhookMessageOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + id: Schema.String, + channel_id: Schema.String, + author: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + pinned: Schema.Boolean, + mention_everyone: Schema.Boolean, + tts: Schema.Boolean, + call: Schema.optional( + Schema.Struct({ + ended_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + participants: Schema.Array(Schema.String), + }), + ), + activity: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + party_id: Schema.optional(Schema.String), + }), + ), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + application_id: Schema.optional(Schema.String), + interaction: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + name_localized: Schema.optional(Schema.String), + }), + ), + nonce: Schema.optional(Schema.Unknown), + webhook_id: Schema.optional(Schema.String), + message_reference: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + channel_id: Schema.String, + message_id: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + }), + ), + thread: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + mention_channels: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + type: Schema.Unknown, + guild_id: Schema.String, + }), + ), + ), + role_subscription_data: Schema.optional( + Schema.Struct({ + role_subscription_listing_id: Schema.String, + tier_name: Schema.String, + total_months_subscribed: Schema.Number, + is_renewal: Schema.Boolean, + }), + ), + purchase_notification: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + guild_product_purchase: Schema.optional( + Schema.Struct({ + listing_id: Schema.String, + product_name: Schema.String, + }), + ), + }), + ), + position: Schema.optional(Schema.Number), + resolved: Schema.optional( + Schema.Struct({ + users: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + ), + members: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + channels: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + ), + roles: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ), + ), + ), + }), + ), + poll: Schema.optional( + Schema.Struct({ + question: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + answers: Schema.Array( + Schema.Struct({ + answer_id: Schema.Number, + poll_media: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + }), + ), + expiry: Schema.String, + allow_multiselect: Schema.Boolean, + layout_type: Schema.Unknown, + results: Schema.Struct({ + answer_counts: Schema.Array( + Schema.Struct({ + id: Schema.Number, + count: Schema.Number, + me_voted: Schema.Boolean, + }), + ), + is_finalized: Schema.Boolean, + }), + }), + ), + shared_client_theme: Schema.optional( + Schema.Struct({ + colors: Schema.Array(Schema.String), + gradient_angle: Schema.Number, + base_mix: Schema.Number, + base_theme: Schema.Unknown, + }), + ), + interaction_metadata: Schema.optional(Schema.Unknown), + message_snapshots: Schema.optional( + Schema.Array( + Schema.Struct({ + message: Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + }), + }), + ), + ), + reactions: Schema.optional( + Schema.Array( + Schema.Struct({ + emoji: Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + count: Schema.Number, + count_details: Schema.Struct({ + burst: Schema.Number, + normal: Schema.Number, + }), + burst_colors: Schema.Array(Schema.String), + me_burst: Schema.Boolean, + me: Schema.Boolean, + }), + ), + ), + referenced_message: Schema.optional(Schema.Unknown), + }); +export type UpdateOriginalWebhookMessageOutput = + typeof UpdateOriginalWebhookMessageOutput.Type; + +// The operation +export const updateOriginalWebhookMessage = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateOriginalWebhookMessageInput, + outputSchema: UpdateOriginalWebhookMessageOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/updateSelfVoiceState.ts b/packages/discord/src/operations/updateSelfVoiceState.ts new file mode 100644 index 000000000..5f9a25760 --- /dev/null +++ b/packages/discord/src/operations/updateSelfVoiceState.ts @@ -0,0 +1,30 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateSelfVoiceStateInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + request_to_speak_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + suppress: Schema.optional(Schema.NullOr(Schema.Boolean)), + channel_id: Schema.optional(Schema.Unknown), + }).pipe( + T.Http({ method: "PATCH", path: "/guilds/{guild_id}/voice-states/@me" }), + ); +export type UpdateSelfVoiceStateInput = typeof UpdateSelfVoiceStateInput.Type; + +// Output Schema +export const UpdateSelfVoiceStateOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type UpdateSelfVoiceStateOutput = typeof UpdateSelfVoiceStateOutput.Type; + +// The operation +export const updateSelfVoiceState = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: UpdateSelfVoiceStateInput, + outputSchema: UpdateSelfVoiceStateOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/updateStageInstance.ts b/packages/discord/src/operations/updateStageInstance.ts new file mode 100644 index 000000000..7875a7067 --- /dev/null +++ b/packages/discord/src/operations/updateStageInstance.ts @@ -0,0 +1,33 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateStageInstanceInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + topic: Schema.optional(Schema.String), + privacy_level: Schema.optional(Schema.Unknown), + }).pipe(T.Http({ method: "PATCH", path: "/stage-instances/{channel_id}" })); +export type UpdateStageInstanceInput = typeof UpdateStageInstanceInput.Type; + +// Output Schema +export const UpdateStageInstanceOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String, + channel_id: Schema.String, + topic: Schema.String, + privacy_level: Schema.Unknown, + id: Schema.String, + discoverable_disabled: Schema.Boolean, + guild_scheduled_event_id: Schema.Unknown, + }); +export type UpdateStageInstanceOutput = typeof UpdateStageInstanceOutput.Type; + +// The operation +export const updateStageInstance = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateStageInstanceInput, + outputSchema: UpdateStageInstanceOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/updateUserMessageExternalModerationMetadata.ts b/packages/discord/src/operations/updateUserMessageExternalModerationMetadata.ts new file mode 100644 index 000000000..5cfd8fb70 --- /dev/null +++ b/packages/discord/src/operations/updateUserMessageExternalModerationMetadata.ts @@ -0,0 +1,36 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateUserMessageExternalModerationMetadataInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + user_id_1: Schema.String.pipe(T.PathParam()), + user_id_2: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "PUT", + path: "/partner-sdk/dms/{user_id_1}/{user_id_2}/messages/{message_id}/moderation-metadata", + }), + ); +export type UpdateUserMessageExternalModerationMetadataInput = + typeof UpdateUserMessageExternalModerationMetadataInput.Type; + +// Output Schema +export const UpdateUserMessageExternalModerationMetadataOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type UpdateUserMessageExternalModerationMetadataOutput = + typeof UpdateUserMessageExternalModerationMetadataOutput.Type; + +// The operation +/** + * Update the external moderation metadata for a user message (DM). + */ +export const updateUserMessageExternalModerationMetadata = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateUserMessageExternalModerationMetadataInput, + outputSchema: UpdateUserMessageExternalModerationMetadataOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + })); diff --git a/packages/discord/src/operations/updateVoiceChannelStatus.ts b/packages/discord/src/operations/updateVoiceChannelStatus.ts new file mode 100644 index 000000000..e2b6603af --- /dev/null +++ b/packages/discord/src/operations/updateVoiceChannelStatus.ts @@ -0,0 +1,33 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateVoiceChannelStatusInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + channel_id: Schema.String.pipe(T.PathParam()), + status: Schema.optional(Schema.NullOr(Schema.String)), + }).pipe( + T.Http({ method: "PUT", path: "/channels/{channel_id}/voice-status" }), + ); +export type UpdateVoiceChannelStatusInput = + typeof UpdateVoiceChannelStatusInput.Type; + +// Output Schema +export const UpdateVoiceChannelStatusOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type UpdateVoiceChannelStatusOutput = + typeof UpdateVoiceChannelStatusOutput.Type; + +// The operation +/** + * Set a voice channel's status. + */ +export const updateVoiceChannelStatus = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: UpdateVoiceChannelStatusInput, + outputSchema: UpdateVoiceChannelStatusOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/updateVoiceState.ts b/packages/discord/src/operations/updateVoiceState.ts new file mode 100644 index 000000000..4d604d708 --- /dev/null +++ b/packages/discord/src/operations/updateVoiceState.ts @@ -0,0 +1,29 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateVoiceStateInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + guild_id: Schema.String.pipe(T.PathParam()), + user_id: Schema.String.pipe(T.PathParam()), + suppress: Schema.optional(Schema.NullOr(Schema.Boolean)), + channel_id: Schema.optional(Schema.Unknown), +}).pipe( + T.Http({ + method: "PATCH", + path: "/guilds/{guild_id}/voice-states/{user_id}", + }), +); +export type UpdateVoiceStateInput = typeof UpdateVoiceStateInput.Type; + +// Output Schema +export const UpdateVoiceStateOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type UpdateVoiceStateOutput = typeof UpdateVoiceStateOutput.Type; + +// The operation +export const updateVoiceState = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateVoiceStateInput, + outputSchema: UpdateVoiceStateOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/updateWebhook.ts b/packages/discord/src/operations/updateWebhook.ts new file mode 100644 index 000000000..9e35bd83e --- /dev/null +++ b/packages/discord/src/operations/updateWebhook.ts @@ -0,0 +1,24 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateWebhookInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + webhook_id: Schema.String.pipe(T.PathParam()), + name: Schema.optional(Schema.String), + avatar: Schema.optional(Schema.NullOr(Schema.String)), + channel_id: Schema.optional(Schema.Unknown), +}).pipe(T.Http({ method: "PATCH", path: "/webhooks/{webhook_id}" })); +export type UpdateWebhookInput = typeof UpdateWebhookInput.Type; + +// Output Schema +export const UpdateWebhookOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Unknown; +export type UpdateWebhookOutput = typeof UpdateWebhookOutput.Type; + +// The operation +export const updateWebhook = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateWebhookInput, + outputSchema: UpdateWebhookOutput, + errors: [BadRequest, Forbidden, NotFound] as const, +})); diff --git a/packages/discord/src/operations/updateWebhookByToken.ts b/packages/discord/src/operations/updateWebhookByToken.ts new file mode 100644 index 000000000..38201b7de --- /dev/null +++ b/packages/discord/src/operations/updateWebhookByToken.ts @@ -0,0 +1,30 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateWebhookByTokenInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + webhook_id: Schema.String.pipe(T.PathParam()), + webhook_token: Schema.String.pipe(T.PathParam()), + name: Schema.optional(Schema.String), + avatar: Schema.optional(Schema.NullOr(Schema.String)), + }).pipe( + T.Http({ method: "PATCH", path: "/webhooks/{webhook_id}/{webhook_token}" }), + ); +export type UpdateWebhookByTokenInput = typeof UpdateWebhookByTokenInput.Type; + +// Output Schema +export const UpdateWebhookByTokenOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Unknown; +export type UpdateWebhookByTokenOutput = typeof UpdateWebhookByTokenOutput.Type; + +// The operation +export const updateWebhookByToken = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: UpdateWebhookByTokenInput, + outputSchema: UpdateWebhookByTokenOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/updateWebhookMessage.ts b/packages/discord/src/operations/updateWebhookMessage.ts new file mode 100644 index 000000000..223e9b857 --- /dev/null +++ b/packages/discord/src/operations/updateWebhookMessage.ts @@ -0,0 +1,914 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UpdateWebhookMessageInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + webhook_id: Schema.String.pipe(T.PathParam()), + webhook_token: Schema.String.pipe(T.PathParam()), + message_id: Schema.String.pipe(T.PathParam()), + thread_id: Schema.optional(Schema.String), + with_components: Schema.optional(Schema.Boolean), + content: Schema.optional(Schema.NullOr(Schema.String)), + embeds: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + type: Schema.optional(Schema.NullOr(Schema.String)), + url: Schema.optional(Schema.NullOr(Schema.String)), + title: Schema.optional(Schema.NullOr(Schema.String)), + color: Schema.optional(Schema.NullOr(Schema.Number)), + timestamp: Schema.optional(Schema.NullOr(Schema.String)), + description: Schema.optional(Schema.NullOr(Schema.String)), + author: Schema.optional(Schema.Unknown), + image: Schema.optional(Schema.Unknown), + thumbnail: Schema.optional(Schema.Unknown), + footer: Schema.optional(Schema.Unknown), + fields: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.optional(Schema.NullOr(Schema.Boolean)), + }), + ), + ), + ), + provider: Schema.optional(Schema.Unknown), + video: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + allowed_mentions: Schema.optional(Schema.Unknown), + components: Schema.optional(Schema.NullOr(Schema.Array(Schema.Unknown))), + attachments: Schema.optional( + Schema.NullOr( + Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.optional(Schema.NullOr(Schema.String)), + description: Schema.optional(Schema.NullOr(Schema.String)), + duration_secs: Schema.optional(Schema.NullOr(Schema.Number)), + waveform: Schema.optional(Schema.NullOr(Schema.String)), + title: Schema.optional(Schema.NullOr(Schema.String)), + is_remix: Schema.optional(Schema.NullOr(Schema.Boolean)), + }), + ), + ), + ), + poll: Schema.optional(Schema.Unknown), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + }).pipe( + T.Http({ + method: "PATCH", + path: "/webhooks/{webhook_id}/{webhook_token}/messages/{message_id}", + }), + ); +export type UpdateWebhookMessageInput = typeof UpdateWebhookMessageInput.Type; + +// Output Schema +export const UpdateWebhookMessageOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + id: Schema.String, + channel_id: Schema.String, + author: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + pinned: Schema.Boolean, + mention_everyone: Schema.Boolean, + tts: Schema.Boolean, + call: Schema.optional( + Schema.Struct({ + ended_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + participants: Schema.Array(Schema.String), + }), + ), + activity: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + party_id: Schema.optional(Schema.String), + }), + ), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + }), + ), + application_id: Schema.optional(Schema.String), + interaction: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + name: Schema.String, + user: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + name_localized: Schema.optional(Schema.String), + }), + ), + nonce: Schema.optional(Schema.Unknown), + webhook_id: Schema.optional(Schema.String), + message_reference: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + channel_id: Schema.String, + message_id: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + }), + ), + thread: Schema.optional( + Schema.Struct({ + id: Schema.String, + type: Schema.Unknown, + last_message_id: Schema.optional(Schema.Unknown), + flags: Schema.Number, + last_pin_timestamp: Schema.optional(Schema.NullOr(Schema.String)), + guild_id: Schema.String, + name: Schema.String, + parent_id: Schema.optional(Schema.Unknown), + rate_limit_per_user: Schema.optional(Schema.Number), + bitrate: Schema.optional(Schema.Number), + user_limit: Schema.optional(Schema.Number), + rtc_region: Schema.optional(Schema.NullOr(Schema.String)), + video_quality_mode: Schema.optional(Schema.Unknown), + permissions: Schema.optional(Schema.String), + owner_id: Schema.String, + thread_metadata: Schema.Struct({ + archived: Schema.Boolean, + archive_timestamp: Schema.NullOr(Schema.String), + auto_archive_duration: Schema.Unknown, + locked: Schema.Boolean, + create_timestamp: Schema.optional(Schema.String), + invitable: Schema.optional(Schema.Boolean), + }), + message_count: Schema.Number, + member_count: Schema.Number, + total_message_sent: Schema.Number, + applied_tags: Schema.optional(Schema.Array(Schema.String)), + member: Schema.optional( + Schema.Struct({ + id: Schema.String, + user_id: Schema.String, + join_timestamp: Schema.String, + flags: Schema.Number, + member: Schema.optional( + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + user: Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + mute: Schema.Boolean, + deaf: Schema.Boolean, + }), + ), + }), + ), + }), + ), + mention_channels: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + type: Schema.Unknown, + guild_id: Schema.String, + }), + ), + ), + role_subscription_data: Schema.optional( + Schema.Struct({ + role_subscription_listing_id: Schema.String, + tier_name: Schema.String, + total_months_subscribed: Schema.Number, + is_renewal: Schema.Boolean, + }), + ), + purchase_notification: Schema.optional( + Schema.Struct({ + type: Schema.Unknown, + guild_product_purchase: Schema.optional( + Schema.Struct({ + listing_id: Schema.String, + product_name: Schema.String, + }), + ), + }), + ), + position: Schema.optional(Schema.Number), + resolved: Schema.optional( + Schema.Struct({ + users: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + ), + members: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + avatar: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + banner: Schema.NullOr(Schema.String), + communication_disabled_until: Schema.NullOr(Schema.String), + flags: Schema.Number, + joined_at: Schema.String, + nick: Schema.NullOr(Schema.String), + pending: Schema.Boolean, + premium_since: Schema.NullOr(Schema.String), + roles: Schema.Array(Schema.String), + collectibles: Schema.optional(Schema.Unknown), + }), + ), + ), + ), + channels: Schema.optional( + Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown)), + ), + roles: Schema.optional( + Schema.NullOr( + Schema.Record( + Schema.String, + Schema.Struct({ + id: Schema.String, + name: Schema.String, + description: Schema.NullOr(Schema.String), + permissions: Schema.String, + position: Schema.Number, + color: Schema.Number, + colors: Schema.Struct({ + primary_color: Schema.Number, + secondary_color: Schema.NullOr(Schema.Number), + tertiary_color: Schema.NullOr(Schema.Number), + }), + hoist: Schema.Boolean, + managed: Schema.Boolean, + mentionable: Schema.Boolean, + icon: Schema.NullOr(Schema.String), + unicode_emoji: Schema.NullOr(Schema.String), + tags: Schema.optional( + Schema.Struct({ + premium_subscriber: Schema.optional(Schema.Unknown), + bot_id: Schema.optional(Schema.String), + integration_id: Schema.optional(Schema.String), + subscription_listing_id: Schema.optional(Schema.String), + available_for_purchase: Schema.optional(Schema.Unknown), + guild_connections: Schema.optional(Schema.Unknown), + }), + ), + flags: Schema.Number, + }), + ), + ), + ), + }), + ), + poll: Schema.optional( + Schema.Struct({ + question: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + answers: Schema.Array( + Schema.Struct({ + answer_id: Schema.Number, + poll_media: Schema.Struct({ + text: Schema.optional(Schema.String), + emoji: Schema.optional( + Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + ), + }), + }), + ), + expiry: Schema.String, + allow_multiselect: Schema.Boolean, + layout_type: Schema.Unknown, + results: Schema.Struct({ + answer_counts: Schema.Array( + Schema.Struct({ + id: Schema.Number, + count: Schema.Number, + me_voted: Schema.Boolean, + }), + ), + is_finalized: Schema.Boolean, + }), + }), + ), + shared_client_theme: Schema.optional( + Schema.Struct({ + colors: Schema.Array(Schema.String), + gradient_angle: Schema.Number, + base_mix: Schema.Number, + base_theme: Schema.Unknown, + }), + ), + interaction_metadata: Schema.optional(Schema.Unknown), + message_snapshots: Schema.optional( + Schema.Array( + Schema.Struct({ + message: Schema.Struct({ + type: Schema.Unknown, + content: Schema.String, + mentions: Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + mention_roles: Schema.Array(Schema.String), + attachments: Schema.Array( + Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional( + Schema.NullOr(Schema.Number), + ), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional( + Schema.NullOr(Schema.Number), + ), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + ), + embeds: Schema.Array( + Schema.Struct({ + type: Schema.String, + url: Schema.optional(Schema.String), + title: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + color: Schema.optional(Schema.Number), + timestamp: Schema.optional(Schema.String), + fields: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.String, + value: Schema.String, + inline: Schema.Boolean, + }), + ), + ), + author: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + provider: Schema.optional( + Schema.Struct({ + name: Schema.String, + url: Schema.optional(Schema.String), + }), + ), + image: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + thumbnail: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + video: Schema.optional( + Schema.Struct({ + url: Schema.optional(Schema.String), + proxy_url: Schema.optional(Schema.String), + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + content_type: Schema.optional(Schema.String), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + description: Schema.optional(Schema.String), + flags: Schema.optional(Schema.Number), + }), + ), + footer: Schema.optional( + Schema.Struct({ + text: Schema.String, + icon_url: Schema.optional(Schema.String), + proxy_icon_url: Schema.optional(Schema.String), + }), + ), + flags: Schema.optional(Schema.NullOr(Schema.Number)), + components: Schema.optional( + Schema.Array( + Schema.Struct({ + type: Schema.Unknown, + id: Schema.Number, + accent_color: Schema.NullOr(Schema.Number), + components: Schema.Array(Schema.Unknown), + spoiler: Schema.Boolean, + }), + ), + ), + }), + ), + timestamp: Schema.String, + edited_timestamp: Schema.NullOr(Schema.String), + flags: Schema.Number, + components: Schema.Array(Schema.Unknown), + stickers: Schema.optional(Schema.Array(Schema.Unknown)), + sticker_items: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + format_type: Schema.Unknown, + }), + ), + ), + }), + }), + ), + ), + reactions: Schema.optional( + Schema.Array( + Schema.Struct({ + emoji: Schema.Struct({ + id: Schema.Unknown, + name: Schema.NullOr(Schema.String), + animated: Schema.optional(Schema.Boolean), + }), + count: Schema.Number, + count_details: Schema.Struct({ + burst: Schema.Number, + normal: Schema.Number, + }), + burst_colors: Schema.Array(Schema.String), + me_burst: Schema.Boolean, + me: Schema.Boolean, + }), + ), + ), + referenced_message: Schema.optional(Schema.Unknown), + }); +export type UpdateWebhookMessageOutput = typeof UpdateWebhookMessageOutput.Type; + +// The operation +export const updateWebhookMessage = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: UpdateWebhookMessageInput, + outputSchema: UpdateWebhookMessageOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/operations/uploadApplicationAttachment.ts b/packages/discord/src/operations/uploadApplicationAttachment.ts new file mode 100644 index 000000000..098525bf7 --- /dev/null +++ b/packages/discord/src/operations/uploadApplicationAttachment.ts @@ -0,0 +1,135 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { BadRequest, Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const UploadApplicationAttachmentInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + application_id: Schema.String.pipe(T.PathParam()), + file: Schema.String, + }).pipe( + T.Http({ + method: "POST", + path: "/applications/{application_id}/attachment", + contentType: "multipart", + }), + ); +export type UploadApplicationAttachmentInput = + typeof UploadApplicationAttachmentInput.Type; + +// Output Schema +export const UploadApplicationAttachmentOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + attachment: Schema.Struct({ + id: Schema.String, + filename: Schema.String, + size: Schema.Number, + url: Schema.String, + proxy_url: Schema.String, + width: Schema.optional(Schema.Number), + height: Schema.optional(Schema.Number), + duration_secs: Schema.optional(Schema.Number), + waveform: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + ephemeral: Schema.optional(Schema.Boolean), + flags: Schema.optional(Schema.Number), + placeholder: Schema.optional(Schema.String), + placeholder_version: Schema.optional(Schema.Number), + title: Schema.optional(Schema.NullOr(Schema.String)), + application: Schema.optional( + Schema.Struct({ + id: Schema.String, + name: Schema.String, + icon: Schema.NullOr(Schema.String), + description: Schema.String, + type: Schema.Unknown, + cover_image: Schema.optional(Schema.String), + primary_sku_id: Schema.optional(Schema.String), + bot: Schema.optional( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + slug: Schema.optional(Schema.String), + guild_id: Schema.optional(Schema.String), + rpc_origins: Schema.optional(Schema.Array(Schema.String)), + bot_public: Schema.optional(Schema.Boolean), + bot_require_code_grant: Schema.optional(Schema.Boolean), + terms_of_service_url: Schema.optional(Schema.String), + privacy_policy_url: Schema.optional(Schema.String), + custom_install_url: Schema.optional(Schema.String), + install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + integration_types_config: Schema.optional( + Schema.Record( + Schema.String, + Schema.Struct({ + oauth2_install_params: Schema.optional( + Schema.Struct({ + scopes: Schema.Array(Schema.Unknown), + permissions: Schema.String, + }), + ), + }), + ), + ), + verify_key: Schema.String, + flags: Schema.Number, + flags_new: Schema.String, + max_participants: Schema.optional(Schema.NullOr(Schema.Number)), + tags: Schema.optional(Schema.Array(Schema.String)), + }), + ), + clip_created_at: Schema.optional(Schema.String), + clip_participants: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.String, + username: Schema.String, + avatar: Schema.NullOr(Schema.String), + discriminator: Schema.String, + public_flags: Schema.Number, + flags: Schema.Number, + bot: Schema.optional(Schema.Boolean), + system: Schema.optional(Schema.Boolean), + banner: Schema.optional(Schema.NullOr(Schema.String)), + accent_color: Schema.optional(Schema.NullOr(Schema.Number)), + global_name: Schema.NullOr(Schema.String), + avatar_decoration_data: Schema.optional(Schema.Unknown), + collectibles: Schema.optional(Schema.Unknown), + primary_guild: Schema.Unknown, + }), + ), + ), + }), + }); +export type UploadApplicationAttachmentOutput = + typeof UploadApplicationAttachmentOutput.Type; + +// The operation +export const uploadApplicationAttachment = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: UploadApplicationAttachmentInput, + outputSchema: UploadApplicationAttachmentOutput, + errors: [BadRequest, Forbidden, NotFound] as const, + }), +); diff --git a/packages/discord/src/retry.ts b/packages/discord/src/retry.ts new file mode 100644 index 000000000..2b83b5c0e --- /dev/null +++ b/packages/discord/src/retry.ts @@ -0,0 +1,47 @@ +/** + * Discord retry configuration. + * + * Defines the per-SDK `Retry` Context.Service tag that + * `packages/discord/src/client.ts` wires into `makeAPI`. Callers can + * install a blanket retry policy at the layer level and have every + * Discord API call below it pick it up. + */ +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { + type Policy, + throttlingFactory, + transientFactory, +} from "@distilled.cloud/core/retry"; + +export { + type Options, + type Factory, + type Policy, + makeDefault, + jittered, + capped, + throttlingOptions, + transientOptions, + throttlingFactory, + transientFactory, +} from "@distilled.cloud/core/retry"; + +/** Context tag for configuring retry behavior of Discord API calls. */ +export class Retry extends Context.Service()("DiscordRetry") {} + +/** Provides a custom retry policy to every Discord API call below it. */ +export const policy = (optionsOrFactory: Policy) => + Effect.provide(Layer.succeed(Retry, optionsOrFactory)); + +/** Disables all automatic retries. */ +export const none = Effect.provide( + Layer.succeed(Retry, { while: () => false }), +); + +/** Apply the throttling retry policy (retries throttling errors indefinitely). */ +export const throttling = policy(throttlingFactory); + +/** Apply the transient retry policy (retries all transient errors indefinitely). */ +export const transient = policy(transientFactory); diff --git a/packages/discord/src/sensitive.ts b/packages/discord/src/sensitive.ts new file mode 100644 index 000000000..2167a39b2 --- /dev/null +++ b/packages/discord/src/sensitive.ts @@ -0,0 +1,4 @@ +/** + * Re-export sensitive data schemas from sdk-core. + */ +export * from "@distilled.cloud/core/sensitive"; diff --git a/packages/discord/src/traits.ts b/packages/discord/src/traits.ts new file mode 100644 index 000000000..cf13e396a --- /dev/null +++ b/packages/discord/src/traits.ts @@ -0,0 +1,4 @@ +/** + * Re-export the shared traits system from sdk-core. + */ +export * from "@distilled.cloud/core/traits"; diff --git a/packages/discord/test/actionGuildJoinRequest.test.ts b/packages/discord/test/actionGuildJoinRequest.test.ts new file mode 100644 index 000000000..5a00b3ce6 --- /dev/null +++ b/packages/discord/test/actionGuildJoinRequest.test.ts @@ -0,0 +1,119 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { actionGuildJoinRequest } from "../src/operations/actionGuildJoinRequest.ts"; +import { getGuildJoinRequests } from "../src/operations/getGuildJoinRequests.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifiers that should not match any real Discord resource. +// Discord IDs are 17–19 digit numeric snowflakes. +const NON_EXISTENT_REQUEST_ID = "100000000000000000"; +const NON_EXISTENT_GUILD_ID = "100000000000000001"; + +describe("actionGuildJoinRequest", () => { + it("happy path - approves a pending guild join request", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the actionGuildJoinRequest happy path", + ); + } + const result = await runEffect( + Effect.gen(function* () { + // Find a pending join request to action. The bot must have Manage Guild + // (or equivalent) on TEST_GUILD_ID and at least one PENDING request must + // exist for this test to exercise the success path. + const list = yield* getGuildJoinRequests({ + guild_id: TEST_GUILD_ID, + status: "PENDING", + }); + const pending = list.guild_join_requests?.[0]; + if (!pending) { + throw new Error( + `No pending join requests in guild ${TEST_GUILD_ID} - cannot exercise happy path. Submit a join request first.`, + ); + } + return yield* actionGuildJoinRequest({ + guild_id: TEST_GUILD_ID, + request_id: pending.id, + action: "APPROVED", + }); + }), + ); + expect(result.id).toBeDefined(); + expect(result.guild_id).toBe(TEST_GUILD_ID); + expect(result.user_id).toBeDefined(); + expect(result.reviewed_at).not.toBeNull(); + }); + + it("error - NotFound for non-existent request_id in real guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + await runEffect( + actionGuildJoinRequest({ + guild_id: TEST_GUILD_ID, + request_id: NON_EXISTENT_REQUEST_ID, + action: "APPROVED", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unknown request_id; some tenants + // surface it as Forbidden if the bot lacks Manage Guild. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) guild_id", async () => { + await runEffect( + actionGuildJoinRequest({ + guild_id: "not-a-snowflake", + request_id: NON_EXISTENT_REQUEST_ID, + action: "APPROVED", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord rejects malformed snowflakes with 400 Invalid Form Body; + // routing layers may also classify it as 404. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for guild bot has no access to", async () => { + await runEffect( + actionGuildJoinRequest({ + guild_id: NON_EXISTENT_GUILD_ID, + request_id: NON_EXISTENT_REQUEST_ID, + action: "APPROVED", + }).pipe( + Effect.flip, + Effect.map((e) => { + // For a guild the bot is not in, Discord typically returns + // Forbidden (50001 Missing Access) but may surface as NotFound. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/addGroupDmUser.test.ts b/packages/discord/test/addGroupDmUser.test.ts new file mode 100644 index 000000000..09632a669 --- /dev/null +++ b/packages/discord/test/addGroupDmUser.test.ts @@ -0,0 +1,116 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { addGroupDmUser } from "../src/operations/addGroupDmUser.ts"; +import { deleteGroupDmUser } from "../src/operations/deleteGroupDmUser.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// Group DM channel the bot/user owns. The endpoint requires a group DM +// the caller created and (for bot tokens) an OAuth2 access_token of the +// user being added with the `gdm.join` scope. +const TEST_GROUP_DM_CHANNEL_ID = process.env.DISCORD_TEST_GROUP_DM_CHANNEL_ID; +const TEST_USER_ID = process.env.DISCORD_TEST_USER_ID; +const TEST_USER_ACCESS_TOKEN = process.env.DISCORD_TEST_USER_ACCESS_TOKEN; + +// Snowflake-format identifiers that should not match real Discord resources. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; +const NON_EXISTENT_USER_ID = "100000000000000001"; + +describe("addGroupDmUser", () => { + it("happy path - adds a user to a group DM and removes them on cleanup", async () => { + if (!TEST_GROUP_DM_CHANNEL_ID || !TEST_USER_ID || !TEST_USER_ACCESS_TOKEN) { + throw new Error( + "DISCORD_TEST_GROUP_DM_CHANNEL_ID, DISCORD_TEST_USER_ID and DISCORD_TEST_USER_ACCESS_TOKEN env vars are required for the addGroupDmUser happy path", + ); + } + await runEffect( + addGroupDmUser({ + channel_id: TEST_GROUP_DM_CHANNEL_ID, + user_id: TEST_USER_ID, + access_token: TEST_USER_ACCESS_TOKEN, + nick: "distilled-test-user", + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + // Discord returns 204 No Content (empty body) on success. + // The operation succeeded if no error was thrown. + expect(result).toBeDefined(); + }), + ), + Effect.ensuring( + deleteGroupDmUser({ + channel_id: TEST_GROUP_DM_CHANNEL_ID, + user_id: TEST_USER_ID, + }).pipe(Effect.ignore), + ), + ), + ); + }); + + it("error - NotFound for non-existent channel_id", async () => { + await runEffect( + addGroupDmUser({ + channel_id: NON_EXISTENT_CHANNEL_ID, + user_id: TEST_USER_ID ?? NON_EXISTENT_USER_ID, + access_token: TEST_USER_ACCESS_TOKEN ?? "fake-access-token", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unknown channels but may surface as + // Forbidden (50001 Missing Access) when the bot can't see the channel. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) channel_id", async () => { + await runEffect( + addGroupDmUser({ + channel_id: "not-a-snowflake", + user_id: TEST_USER_ID ?? NON_EXISTENT_USER_ID, + access_token: TEST_USER_ACCESS_TOKEN ?? "fake-access-token", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord rejects malformed snowflakes with 400 Invalid Form Body; + // routing layers may also classify it as 404. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for channel the caller does not own", async () => { + await runEffect( + addGroupDmUser({ + channel_id: NON_EXISTENT_CHANNEL_ID, + user_id: NON_EXISTENT_USER_ID, + access_token: "fake-access-token", + nick: "should-fail", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns Forbidden (50001 Missing Access) for group DMs + // the caller does not own; for entirely unknown channel snowflakes + // it may surface as NotFound instead. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/addGuildMember.test.ts b/packages/discord/test/addGuildMember.test.ts new file mode 100644 index 000000000..d87e4c019 --- /dev/null +++ b/packages/discord/test/addGuildMember.test.ts @@ -0,0 +1,126 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { addGuildMember } from "../src/operations/addGuildMember.ts"; +import { deleteGuildMember } from "../src/operations/deleteGuildMember.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// The endpoint requires: +// - a guild the bot is in with CREATE_INSTANT_INVITE permission +// - a user OAuth2 access_token with the `guilds.join` scope +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_USER_ID = process.env.DISCORD_TEST_USER_ID; +const TEST_USER_ACCESS_TOKEN = process.env.DISCORD_TEST_USER_ACCESS_TOKEN; + +// Snowflake-format identifiers that should not match real Discord resources. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_USER_ID = "100000000000000001"; + +describe("addGuildMember", () => { + it("happy path - adds a user to a guild and removes them on cleanup", async () => { + if (!TEST_GUILD_ID || !TEST_USER_ID || !TEST_USER_ACCESS_TOKEN) { + throw new Error( + "DISCORD_TEST_GUILD_ID, DISCORD_TEST_USER_ID and DISCORD_TEST_USER_ACCESS_TOKEN env vars are required for the addGuildMember happy path", + ); + } + await runEffect( + addGuildMember({ + guild_id: TEST_GUILD_ID, + user_id: TEST_USER_ID, + access_token: TEST_USER_ACCESS_TOKEN, + nick: "distilled-test-member", + }).pipe( + Effect.tap((member) => + Effect.sync(() => { + // Discord returns 201 with the member object on add, or 204 with + // empty body if the user is already a member. With a non-empty + // body, the member.user.id must equal the requested user_id. + expect(member.user.id).toBe(TEST_USER_ID); + expect(Array.isArray(member.roles)).toBe(true); + expect(typeof member.joined_at).toBe("string"); + }), + ), + Effect.ensuring( + deleteGuildMember({ + guild_id: TEST_GUILD_ID, + user_id: TEST_USER_ID, + }).pipe(Effect.ignore), + ), + ), + ); + }); + + it("error - NotFound for non-existent guild_id", async () => { + await runEffect( + addGuildMember({ + guild_id: NON_EXISTENT_GUILD_ID, + user_id: TEST_USER_ID ?? NON_EXISTENT_USER_ID, + access_token: TEST_USER_ACCESS_TOKEN ?? "fake-access-token", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unseen guilds, but may surface as + // Forbidden (50001 Missing Access) when the bot can't see the guild. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) guild_id", async () => { + await runEffect( + addGuildMember({ + guild_id: "not-a-snowflake", + user_id: TEST_USER_ID ?? NON_EXISTENT_USER_ID, + access_token: TEST_USER_ACCESS_TOKEN ?? "fake-access-token", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord rejects malformed snowflakes with 400 Invalid Form Body; + // routing layers may also classify it as 404. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when access_token is invalid", async () => { + if (!TEST_GUILD_ID || !TEST_USER_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_USER_ID env vars are required for the Forbidden test", + ); + } + await runEffect( + addGuildMember({ + guild_id: TEST_GUILD_ID, + user_id: TEST_USER_ID, + access_token: "definitely-not-a-valid-oauth2-access-token", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns Forbidden (50025 Invalid OAuth2 access token) when + // the access_token is invalid or lacks the `guilds.join` scope; it + // may also surface as BadRequest depending on validation order. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "BadRequest", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/addGuildMemberRole.test.ts b/packages/discord/test/addGuildMemberRole.test.ts new file mode 100644 index 000000000..de039d6b5 --- /dev/null +++ b/packages/discord/test/addGuildMemberRole.test.ts @@ -0,0 +1,126 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { addGuildMemberRole } from "../src/operations/addGuildMemberRole.ts"; +import { deleteGuildMemberRole } from "../src/operations/deleteGuildMemberRole.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// The endpoint requires: +// - the bot to be in TEST_GUILD_ID with MANAGE_ROLES permission +// - TEST_USER_ID to already be a guild member +// - TEST_ROLE_ID to be a role lower than the bot's highest role +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_USER_ID = process.env.DISCORD_TEST_USER_ID; +const TEST_ROLE_ID = process.env.DISCORD_TEST_ROLE_ID; + +// Snowflake-format identifiers that should not match real Discord resources. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_USER_ID = "100000000000000001"; +const NON_EXISTENT_ROLE_ID = "100000000000000002"; + +describe("addGuildMemberRole", () => { + it("happy path - adds a role to a guild member and removes it on cleanup", async () => { + if (!TEST_GUILD_ID || !TEST_USER_ID || !TEST_ROLE_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID, DISCORD_TEST_USER_ID and DISCORD_TEST_ROLE_ID env vars are required for the addGuildMemberRole happy path", + ); + } + await runEffect( + addGuildMemberRole({ + guild_id: TEST_GUILD_ID, + user_id: TEST_USER_ID, + role_id: TEST_ROLE_ID, + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + // Discord returns 204 No Content on success — the operation + // succeeded if no error was thrown. + expect(result).toBeUndefined(); + }), + ), + Effect.ensuring( + deleteGuildMemberRole({ + guild_id: TEST_GUILD_ID, + user_id: TEST_USER_ID, + role_id: TEST_ROLE_ID, + }).pipe(Effect.ignore), + ), + ), + ); + }); + + it("error - NotFound for non-existent role_id", async () => { + if (!TEST_GUILD_ID || !TEST_USER_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_USER_ID env vars are required for the NotFound test", + ); + } + await runEffect( + addGuildMemberRole({ + guild_id: TEST_GUILD_ID, + user_id: TEST_USER_ID, + role_id: NON_EXISTENT_ROLE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for an unseen role but may surface as + // Forbidden when the bot can't manage the role. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) role_id", async () => { + if (!TEST_GUILD_ID || !TEST_USER_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_USER_ID env vars are required for the BadRequest test", + ); + } + await runEffect( + addGuildMemberRole({ + guild_id: TEST_GUILD_ID, + user_id: TEST_USER_ID, + role_id: "not-a-snowflake", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord rejects malformed snowflakes with 400 Invalid Form Body; + // routing layers may also classify it as 404. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for guild bot has no access to", async () => { + await runEffect( + addGuildMemberRole({ + guild_id: NON_EXISTENT_GUILD_ID, + user_id: NON_EXISTENT_USER_ID, + role_id: NON_EXISTENT_ROLE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // For a guild the bot is not in, Discord typically returns + // Forbidden (50001 Missing Access) but may surface as NotFound. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/addLobbyMember.test.ts b/packages/discord/test/addLobbyMember.test.ts new file mode 100644 index 000000000..0b3c91142 --- /dev/null +++ b/packages/discord/test/addLobbyMember.test.ts @@ -0,0 +1,129 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { addLobbyMember } from "../src/operations/addLobbyMember.ts"; +import { createLobby } from "../src/operations/createLobby.ts"; +import { deleteLobbyMember } from "../src/operations/deleteLobbyMember.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// Short random hex string generated once per test run. +// Append this to resource metadata so parallel test runs don't collide. +const testRunId: string = crypto + .randomUUID() + .replace(/-/g, "") + .slice(0, 8); + +// A real Discord user_id is required because Discord validates the snowflake +// against an existing user account before adding them to the lobby. +const TEST_USER_ID = process.env.DISCORD_TEST_USER_ID; + +// Snowflake-format identifiers that should not match real Discord resources. +const NON_EXISTENT_LOBBY_ID = "100000000000000000"; +const NON_EXISTENT_USER_ID = "100000000000000001"; + +describe("addLobbyMember", () => { + it("happy path - creates a lobby, adds a member, removes them on cleanup", async () => { + if (!TEST_USER_ID) { + throw new Error( + "DISCORD_TEST_USER_ID env var is required for the addLobbyMember happy path", + ); + } + await runEffect( + Effect.gen(function* () { + // Create a short-lived lobby owned by this application. Lobbies + // auto-expire after idle_timeout_seconds with no activity, so no + // explicit lobby teardown is required. + const lobby = yield* createLobby({ + idle_timeout_seconds: 60, + metadata: { test_run_id: testRunId, purpose: "addLobbyMember-test" }, + }); + return yield* addLobbyMember({ + lobby_id: lobby.id, + user_id: TEST_USER_ID, + metadata: { test_run_id: testRunId }, + }).pipe( + Effect.tap((member) => + Effect.sync(() => { + expect(member.id).toBe(TEST_USER_ID); + expect(typeof member.flags).toBe("number"); + expect(member.metadata).not.toBeNull(); + expect(member.metadata?.test_run_id).toBe(testRunId); + }), + ), + Effect.ensuring( + deleteLobbyMember({ + lobby_id: lobby.id, + user_id: TEST_USER_ID, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent lobby_id", async () => { + await runEffect( + addLobbyMember({ + lobby_id: NON_EXISTENT_LOBBY_ID, + user_id: TEST_USER_ID ?? NON_EXISTENT_USER_ID, + metadata: { test_run_id: testRunId }, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for an unseen lobby but may surface as + // Forbidden when the application can't access the lobby. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) lobby_id", async () => { + await runEffect( + addLobbyMember({ + lobby_id: "not-a-snowflake", + user_id: TEST_USER_ID ?? NON_EXISTENT_USER_ID, + metadata: { test_run_id: testRunId }, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord rejects malformed snowflakes with 400 Invalid Form Body; + // routing layers may also classify it as 404. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for lobby owned by a different application", async () => { + await runEffect( + addLobbyMember({ + lobby_id: NON_EXISTENT_LOBBY_ID, + user_id: NON_EXISTENT_USER_ID, + metadata: { test_run_id: testRunId }, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns Forbidden (50001 Missing Access) for lobbies the + // application does not own; for fully unknown snowflakes it may + // surface as NotFound instead. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/addMyMessageReaction.test.ts b/packages/discord/test/addMyMessageReaction.test.ts new file mode 100644 index 000000000..ec54c5fe3 --- /dev/null +++ b/packages/discord/test/addMyMessageReaction.test.ts @@ -0,0 +1,138 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { addMyMessageReaction } from "../src/operations/addMyMessageReaction.ts"; +import { createMessage } from "../src/operations/createMessage.ts"; +import { deleteMessage } from "../src/operations/deleteMessage.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// Short random hex string generated once per test run. +// Append this to message content so parallel test runs don't collide. +const testRunId: string = crypto + .randomUUID() + .replace(/-/g, "") + .slice(0, 8); + +// A real Discord channel where the bot can send and react to messages. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifiers that should not match real Discord resources. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; +const NON_EXISTENT_MESSAGE_ID = "100000000000000001"; + +// Unicode thumbs-up emoji. The PathParam trait URL-encodes this for Discord. +const REACTION_EMOJI = "👍"; + +describe("addMyMessageReaction", () => { + it("happy path - posts a message and adds a reaction to it", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the addMyMessageReaction happy path", + ); + } + await runEffect( + Effect.gen(function* () { + // Post a fresh message we own so the reaction can be added cleanly. + const message = yield* createMessage({ + channel_id: TEST_CHANNEL_ID, + content: `distilled-test-react-${testRunId}`, + }); + return yield* addMyMessageReaction({ + channel_id: TEST_CHANNEL_ID, + message_id: message.id, + emoji_name: REACTION_EMOJI, + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + // Discord returns 204 No Content on success — the operation + // succeeded if no error was thrown. + expect(result).toBeUndefined(); + }), + ), + Effect.ensuring( + // Deleting the message also removes all reactions on it. + deleteMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: message.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent message_id in real channel", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the NotFound test", + ); + } + await runEffect( + addMyMessageReaction({ + channel_id: TEST_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + emoji_name: REACTION_EMOJI, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for an unseen message but may surface as + // Forbidden when the bot can't read the channel. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) message_id", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the BadRequest test", + ); + } + await runEffect( + addMyMessageReaction({ + channel_id: TEST_CHANNEL_ID, + message_id: "not-a-snowflake", + emoji_name: REACTION_EMOJI, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord rejects malformed snowflakes with 400 Invalid Form Body; + // routing layers may also classify it as 404. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for channel bot has no access to", async () => { + await runEffect( + addMyMessageReaction({ + channel_id: NON_EXISTENT_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + emoji_name: REACTION_EMOJI, + }).pipe( + Effect.flip, + Effect.map((e) => { + // For a channel the bot is not in, Discord typically returns + // Forbidden (50001 Missing Access) but may surface as NotFound. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/addThreadMember.test.ts b/packages/discord/test/addThreadMember.test.ts new file mode 100644 index 000000000..bb4338f89 --- /dev/null +++ b/packages/discord/test/addThreadMember.test.ts @@ -0,0 +1,108 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { addThreadMember } from "../src/operations/addThreadMember.ts"; +import { deleteThreadMember } from "../src/operations/deleteThreadMember.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// A real Discord thread (channel) the bot can manage members in, and a +// real Discord user_id to add. The bot must already be a member of the +// thread (or have permission to manage it). +const TEST_THREAD_ID = process.env.DISCORD_TEST_THREAD_ID; +const TEST_USER_ID = process.env.DISCORD_TEST_USER_ID; + +// Snowflake-format identifiers that should not match real Discord resources. +const NON_EXISTENT_THREAD_ID = "100000000000000000"; +const NON_EXISTENT_USER_ID = "100000000000000001"; + +describe("addThreadMember", () => { + it("happy path - adds a user to a thread and removes them on cleanup", async () => { + if (!TEST_THREAD_ID || !TEST_USER_ID) { + throw new Error( + "DISCORD_TEST_THREAD_ID and DISCORD_TEST_USER_ID env vars are required for the addThreadMember happy path", + ); + } + await runEffect( + addThreadMember({ + channel_id: TEST_THREAD_ID, + user_id: TEST_USER_ID, + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + // Discord returns 204 No Content on success — the operation + // succeeded if no error was thrown. + expect(result).toBeUndefined(); + }), + ), + Effect.ensuring( + deleteThreadMember({ + channel_id: TEST_THREAD_ID, + user_id: TEST_USER_ID, + }).pipe(Effect.ignore), + ), + ), + ); + }); + + it("error - NotFound for non-existent thread channel_id", async () => { + await runEffect( + addThreadMember({ + channel_id: NON_EXISTENT_THREAD_ID, + user_id: TEST_USER_ID ?? NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for an unseen thread but may surface as + // Forbidden when the bot can't see the channel. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) channel_id", async () => { + await runEffect( + addThreadMember({ + channel_id: "not-a-snowflake", + user_id: TEST_USER_ID ?? NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord rejects malformed snowflakes with 400 Invalid Form Body; + // routing layers may also classify it as 404. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for thread bot has no access to", async () => { + await runEffect( + addThreadMember({ + channel_id: NON_EXISTENT_THREAD_ID, + user_id: NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // For a thread the bot is not in, Discord typically returns + // Forbidden (50001 Missing Access) but may surface as NotFound. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/applicationsGetActivityInstance.test.ts b/packages/discord/test/applicationsGetActivityInstance.test.ts new file mode 100644 index 000000000..d22cca191 --- /dev/null +++ b/packages/discord/test/applicationsGetActivityInstance.test.ts @@ -0,0 +1,88 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { applicationsGetActivityInstance } from "../src/operations/applicationsGetActivityInstance.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// The endpoint requires a real running activity instance launched by the +// caller's application. Activity instances are created when users open an +// embedded activity in a voice channel. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; +const TEST_ACTIVITY_INSTANCE_ID = + process.env.DISCORD_TEST_ACTIVITY_INSTANCE_ID; + +// Snowflake-format identifiers that should not match real Discord resources. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; +const NON_EXISTENT_INSTANCE_ID = "i-does-not-exist-12345678"; + +describe("applicationsGetActivityInstance", () => { + it("happy path - fetches a running activity instance for the application", async () => { + if (!TEST_APPLICATION_ID || !TEST_ACTIVITY_INSTANCE_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID and DISCORD_TEST_ACTIVITY_INSTANCE_ID env vars are required for the applicationsGetActivityInstance happy path", + ); + } + const result = await runEffect( + applicationsGetActivityInstance({ + application_id: TEST_APPLICATION_ID, + instance_id: TEST_ACTIVITY_INSTANCE_ID, + }), + ); + expect(result.application_id).toBe(TEST_APPLICATION_ID); + expect(result.instance_id).toBe(TEST_ACTIVITY_INSTANCE_ID); + expect(typeof result.launch_id).toBe("string"); + expect(Array.isArray(result.users)).toBe(true); + }); + + it("error - NotFound for non-existent instance_id", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the NotFound test", + ); + } + await runEffect( + applicationsGetActivityInstance({ + application_id: TEST_APPLICATION_ID, + instance_id: NON_EXISTENT_INSTANCE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for an instance the application has no + // record of, but may surface as Forbidden if the application can't + // see the instance at all. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for application_id the caller does not own", async () => { + await runEffect( + applicationsGetActivityInstance({ + application_id: NON_EXISTENT_APPLICATION_ID, + instance_id: NON_EXISTENT_INSTANCE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns Forbidden (50001 Missing Access) for applications + // the caller does not own; for fully unknown snowflakes it may + // surface as NotFound instead. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/banUserFromGuild.test.ts b/packages/discord/test/banUserFromGuild.test.ts new file mode 100644 index 000000000..23fd5e4de --- /dev/null +++ b/packages/discord/test/banUserFromGuild.test.ts @@ -0,0 +1,132 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { banUserFromGuild } from "../src/operations/banUserFromGuild.ts"; +import { unbanUserFromGuild } from "../src/operations/unbanUserFromGuild.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// The endpoint requires: +// - a guild the bot is in with BAN_MEMBERS permission +// - a user_id (snowflake) of a user the bot is allowed to ban +// (typically a throwaway test user not in the guild — Discord allows +// banning users who are not currently a member of the guild) +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_BAN_USER_ID = process.env.DISCORD_TEST_BAN_USER_ID; + +// Snowflake-format identifiers that should not match real Discord resources. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_USER_ID = "100000000000000001"; + +describe("banUserFromGuild", () => { + it("happy path - bans a user and unbans them on cleanup", async () => { + if (!TEST_GUILD_ID || !TEST_BAN_USER_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_BAN_USER_ID env vars are required for the banUserFromGuild happy path", + ); + } + await runEffect( + banUserFromGuild({ + guild_id: TEST_GUILD_ID, + user_id: TEST_BAN_USER_ID, + delete_message_seconds: 0, + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + // Discord returns 204 No Content on success; the SDK decodes it to + // void / undefined. + expect(result).toBeUndefined(); + }), + ), + Effect.ensuring( + unbanUserFromGuild({ + guild_id: TEST_GUILD_ID, + user_id: TEST_BAN_USER_ID, + }).pipe(Effect.ignore), + ), + ), + ); + }); + + it("error - NotFound for non-existent guild_id", async () => { + await runEffect( + banUserFromGuild({ + guild_id: NON_EXISTENT_GUILD_ID, + user_id: TEST_BAN_USER_ID ?? NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unseen guilds, but may surface as + // Forbidden (50001 Missing Access) when the bot can't see the guild. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for invalid delete_message_seconds (out of range)", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the BadRequest test", + ); + } + await runEffect( + banUserFromGuild({ + guild_id: TEST_GUILD_ID, + user_id: TEST_BAN_USER_ID ?? NON_EXISTENT_USER_ID, + // Discord's docs cap delete_message_seconds at 604800 (7 days). + // Anything well beyond that range should be rejected as 400 Invalid + // Form Body. May also surface as Forbidden if a permission check + // fires before form validation. + delete_message_seconds: 99_999_999, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when banning a user_id the bot lacks permission to ban", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the Forbidden test", + ); + } + // Attempting to ban an arbitrary snowflake the bot does not own and + // cannot manage typically yields Forbidden (50013 Missing Permissions) + // when the target's role is higher than the bot's, or NotFound when the + // user does not exist at all. + await runEffect( + banUserFromGuild({ + guild_id: TEST_GUILD_ID, + user_id: NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/botPartnerSdkToken.test.ts b/packages/discord/test/botPartnerSdkToken.test.ts new file mode 100644 index 000000000..972f3007b --- /dev/null +++ b/packages/discord/test/botPartnerSdkToken.test.ts @@ -0,0 +1,110 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { botPartnerSdkToken } from "../src/operations/botPartnerSdkToken.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint requires: +// - the bot's application to be enrolled in Discord's Partner SDK / Social +// Layer program (otherwise Discord returns 403 Forbidden). +// - an arbitrary external_user_id that the partner uses to identify a user +// in their own system. +const TEST_EXTERNAL_USER_ID = + process.env.DISCORD_TEST_EXTERNAL_USER_ID ?? + `distilled-discord-partner-${testRunId}`; + +describe("botPartnerSdkToken", () => { + it("happy path - mints a partner SDK token for an external user", async () => { + await runEffect( + botPartnerSdkToken({ + external_user_id: TEST_EXTERNAL_USER_ID, + preferred_global_name: `distilled-test-${testRunId}`, + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + expect(typeof result.token_type).toBe("string"); + expect(typeof result.expires_in).toBe("number"); + expect(typeof result.scope).toBe("string"); + expect(typeof result.id_token).toBe("string"); + // access_token is a SensitiveString — assert it exists. + expect(result.access_token).toBeDefined(); + }), + ), + ), + ); + }); + + it("error - BadRequest for missing external_user_id (empty string)", async () => { + await runEffect( + botPartnerSdkToken({ + external_user_id: "", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord rejects empty external_user_id with 400 Invalid Form Body; + // bots not enrolled in Partner SDK may instead receive 403 Forbidden + // before form validation runs. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when the application is not enrolled in Partner SDK", async () => { + // A bot whose application is not allowlisted for the Partner SDK / Social + // Layer program receives 403 Forbidden on this endpoint regardless of the + // input. May surface as NotFound if the route itself is gated. + await runEffect( + botPartnerSdkToken({ + external_user_id: `distilled-forbidden-${testRunId}`, + preferred_global_name: null, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for a comically long external_user_id that no route accepts", async () => { + // Discord's Partner SDK route can reject extreme inputs as NotFound when + // the gateway treats them as a routing miss; otherwise the API surfaces + // BadRequest or Forbidden. + await runEffect( + botPartnerSdkToken({ + external_user_id: "x".repeat(4096), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "BadRequest", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/botPartnerSdkUnmergeProvisionalAccount.test.ts b/packages/discord/test/botPartnerSdkUnmergeProvisionalAccount.test.ts new file mode 100644 index 000000000..2cdb1c8b7 --- /dev/null +++ b/packages/discord/test/botPartnerSdkUnmergeProvisionalAccount.test.ts @@ -0,0 +1,105 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { botPartnerSdkUnmergeProvisionalAccount } from "../src/operations/botPartnerSdkUnmergeProvisionalAccount.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint requires: +// - the bot's application to be enrolled in Discord's Partner SDK / Social +// Layer program (otherwise Discord returns 403 Forbidden). +// - an external_user_id whose provisional account was previously merged +// and can now be unmerged. +const TEST_EXTERNAL_USER_ID = + process.env.DISCORD_TEST_EXTERNAL_USER_ID ?? + `distilled-discord-partner-${testRunId}`; + +describe("botPartnerSdkUnmergeProvisionalAccount", () => { + it("happy path - unmerges a provisional account for an external user", async () => { + await runEffect( + botPartnerSdkUnmergeProvisionalAccount({ + external_user_id: TEST_EXTERNAL_USER_ID, + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + // Discord returns 204 No Content on success; the SDK decodes it + // to void / undefined. + expect(result).toBeUndefined(); + }), + ), + ), + ); + }); + + it("error - BadRequest for missing external_user_id (empty string)", async () => { + await runEffect( + botPartnerSdkUnmergeProvisionalAccount({ + external_user_id: "", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord rejects empty external_user_id with 400 Invalid Form Body; + // bots not enrolled in Partner SDK may instead receive 403 Forbidden + // before form validation runs, or 404 if the route is gated. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when the application is not enrolled in Partner SDK", async () => { + // A bot whose application is not allowlisted for the Partner SDK / Social + // Layer program receives 403 Forbidden on this endpoint regardless of the + // input. May surface as NotFound if the route itself is gated. + await runEffect( + botPartnerSdkUnmergeProvisionalAccount({ + external_user_id: `distilled-forbidden-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for an external_user_id that has no merged provisional account", async () => { + // An external_user_id that was never merged into a provisional account + // typically returns 404 Not Found; some routing layers classify it as + // 400 Invalid Form Body or 403 Forbidden if the bot is not enrolled. + await runEffect( + botPartnerSdkUnmergeProvisionalAccount({ + external_user_id: `distilled-never-merged-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "BadRequest", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/bulkBanUsersFromGuild.test.ts b/packages/discord/test/bulkBanUsersFromGuild.test.ts new file mode 100644 index 000000000..37e622875 --- /dev/null +++ b/packages/discord/test/bulkBanUsersFromGuild.test.ts @@ -0,0 +1,151 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { bulkBanUsersFromGuild } from "../src/operations/bulkBanUsersFromGuild.ts"; +import { unbanUserFromGuild } from "../src/operations/unbanUserFromGuild.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// The endpoint requires: +// - a guild the bot is in with BAN_MEMBERS + MANAGE_GUILD permissions. +// - a comma-separated list of user_id snowflakes for the happy path. +// Discord allows banning users not currently in the guild, so a +// throwaway snowflake suffices for testing. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_BAN_USER_IDS = process.env.DISCORD_TEST_BULK_BAN_USER_IDS; + +// Snowflake-format identifiers that should not match real Discord resources. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_USER_ID_A = "100000000000000001"; +const NON_EXISTENT_USER_ID_B = "100000000000000002"; + +describe("bulkBanUsersFromGuild", () => { + it("happy path - bulk-bans a list of users and unbans them on cleanup", async () => { + if (!TEST_GUILD_ID || !TEST_BAN_USER_IDS) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_BULK_BAN_USER_IDS env vars are required for the bulkBanUsersFromGuild happy path", + ); + } + const userIds = TEST_BAN_USER_IDS.split(",") + .map((s) => s.trim()) + .filter((s) => s.length > 0); + if (userIds.length === 0) { + throw new Error( + "DISCORD_TEST_BULK_BAN_USER_IDS must contain at least one snowflake", + ); + } + await runEffect( + bulkBanUsersFromGuild({ + guild_id: TEST_GUILD_ID, + user_ids: userIds, + delete_message_seconds: 0, + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + expect(Array.isArray(result.banned_users)).toBe(true); + expect(Array.isArray(result.failed_users)).toBe(true); + // Every ID in the request must appear in exactly one of the two + // result arrays. + const seen = new Set([ + ...result.banned_users, + ...result.failed_users, + ]); + for (const id of userIds) { + expect(seen.has(id)).toBe(true); + } + }), + ), + Effect.ensuring( + Effect.forEach( + userIds, + (user_id) => + unbanUserFromGuild({ + guild_id: TEST_GUILD_ID, + user_id, + }).pipe(Effect.ignore), + { concurrency: 4 }, + ), + ), + ), + ); + }); + + it("error - NotFound for non-existent guild_id", async () => { + await runEffect( + bulkBanUsersFromGuild({ + guild_id: NON_EXISTENT_GUILD_ID, + user_ids: [NON_EXISTENT_USER_ID_A, NON_EXISTENT_USER_ID_B], + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unseen guilds, but may surface as + // Forbidden (50001 Missing Access) when the bot can't see the guild. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for empty user_ids array", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the BadRequest test", + ); + } + await runEffect( + bulkBanUsersFromGuild({ + guild_id: TEST_GUILD_ID, + user_ids: [], + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord requires 1..200 user_ids per call; an empty array yields + // 400 Invalid Form Body. May also surface as Forbidden if the bot + // lacks BAN_MEMBERS, or NotFound for an unseen guild. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when the bot lacks BAN_MEMBERS / MANAGE_GUILD permissions", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the Forbidden test", + ); + } + // Targeting an arbitrary snowflake the bot does not own typically yields + // Forbidden (50013 Missing Permissions); may also surface as NotFound or + // BadRequest depending on validation order. + await runEffect( + bulkBanUsersFromGuild({ + guild_id: TEST_GUILD_ID, + user_ids: [NON_EXISTENT_USER_ID_A], + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/bulkDeleteMessages.test.ts b/packages/discord/test/bulkDeleteMessages.test.ts new file mode 100644 index 000000000..43d3d1b32 --- /dev/null +++ b/packages/discord/test/bulkDeleteMessages.test.ts @@ -0,0 +1,127 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { bulkDeleteMessages } from "../src/operations/bulkDeleteMessages.ts"; +import { createMessage } from "../src/operations/createMessage.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint requires: +// - a guild text channel the bot is in with MANAGE_MESSAGES permission. +// - 2..100 message IDs no older than 14 days. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifiers that should not match real Discord resources. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; +const NON_EXISTENT_MESSAGE_ID_A = "100000000000000001"; +const NON_EXISTENT_MESSAGE_ID_B = "100000000000000002"; + +describe("bulkDeleteMessages", () => { + it("happy path - bulk-deletes two freshly created messages", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the bulkDeleteMessages happy path", + ); + } + await runEffect( + Effect.gen(function* () { + const m1 = yield* createMessage({ + channel_id: TEST_CHANNEL_ID, + content: `distilled-bulk-delete-1-${testRunId}`, + }); + const m2 = yield* createMessage({ + channel_id: TEST_CHANNEL_ID, + content: `distilled-bulk-delete-2-${testRunId}`, + }); + const result = yield* bulkDeleteMessages({ + channel_id: TEST_CHANNEL_ID, + messages: [m1.id, m2.id], + }); + // Discord returns 204 No Content; SDK decodes to void / undefined. + expect(result).toBeUndefined(); + }), + ); + }); + + it("error - NotFound for non-existent channel_id", async () => { + await runEffect( + bulkDeleteMessages({ + channel_id: NON_EXISTENT_CHANNEL_ID, + messages: [NON_EXISTENT_MESSAGE_ID_A, NON_EXISTENT_MESSAGE_ID_B], + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unseen channels, but may surface as + // Forbidden (50001 Missing Access) when the bot can't see it. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for too few messages (only one supplied)", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the BadRequest test", + ); + } + // Discord requires 2..100 message IDs; a single-element array is rejected + // with 400 Invalid Form Body. May also surface as Forbidden if the bot + // lacks MANAGE_MESSAGES, or NotFound for an unseen channel. + await runEffect( + bulkDeleteMessages({ + channel_id: TEST_CHANNEL_ID, + messages: [NON_EXISTENT_MESSAGE_ID_A], + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when the bot lacks MANAGE_MESSAGES on the channel", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the Forbidden test", + ); + } + // Targeting message snowflakes the bot does not own typically yields + // Forbidden (50013 Missing Permissions) once the channel is reachable; + // may also surface as NotFound (10008) since the messages do not exist + // in the channel, or BadRequest if Discord rejects the IDs as too old. + await runEffect( + bulkDeleteMessages({ + channel_id: TEST_CHANNEL_ID, + messages: [NON_EXISTENT_MESSAGE_ID_A, NON_EXISTENT_MESSAGE_ID_B], + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/bulkSetApplicationCommands.test.ts b/packages/discord/test/bulkSetApplicationCommands.test.ts new file mode 100644 index 000000000..f86b4faa0 --- /dev/null +++ b/packages/discord/test/bulkSetApplicationCommands.test.ts @@ -0,0 +1,120 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { bulkSetApplicationCommands } from "../src/operations/bulkSetApplicationCommands.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// The endpoint requires: +// - the bot's application_id (snowflake). +// The SDK's input schema currently only exposes the path parameter +// (application_id) and not the request body. As a result the operation +// performs a PUT with no body, which Discord treats as "set commands to +// no commands" — a destructive global-commands wipe. That is acceptable +// for test-only applications. Operators must opt in with +// DISCORD_TEST_APPLICATION_ID + DISCORD_TEST_ALLOW_DESTRUCTIVE_COMMANDS=1. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; +const ALLOW_DESTRUCTIVE = + process.env.DISCORD_TEST_ALLOW_DESTRUCTIVE_COMMANDS === "1"; + +// Snowflake-format identifier that should not match a real application. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; + +describe("bulkSetApplicationCommands", () => { + it("happy path - bulk-sets the application's global commands", async () => { + if (!TEST_APPLICATION_ID || !ALLOW_DESTRUCTIVE) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID and DISCORD_TEST_ALLOW_DESTRUCTIVE_COMMANDS=1 are required for the bulkSetApplicationCommands happy path (this PUT replaces ALL global commands for the application).", + ); + } + await runEffect( + bulkSetApplicationCommands({ + application_id: TEST_APPLICATION_ID, + }).pipe( + Effect.tap((commands) => + Effect.sync(() => { + // Discord returns an array of the resulting application commands. + expect(Array.isArray(commands)).toBe(true); + for (const c of commands) { + expect(typeof c.id).toBe("string"); + expect(typeof c.application_id).toBe("string"); + expect(typeof c.name).toBe("string"); + expect(typeof c.description).toBe("string"); + expect(c.application_id).toBe(TEST_APPLICATION_ID); + } + }), + ), + ), + ); + }); + + it("error - NotFound for non-existent application_id", async () => { + await runEffect( + bulkSetApplicationCommands({ + application_id: NON_EXISTENT_APPLICATION_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for an unseen application_id, but may + // surface as Forbidden when the bot's token does not own the + // application, or BadRequest if the route rejects the empty body. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) application_id", async () => { + await runEffect( + bulkSetApplicationCommands({ + application_id: "not-a-snowflake", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord rejects malformed snowflakes with 400 Invalid Form Body; + // routing layers may also classify it as 404, or the bot may lack + // ownership and receive 403. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when the bot does not own the application_id", async () => { + // Calling against a real application_id that the bot's token does not + // own typically yields 403 Forbidden. Here we use a snowflake-shaped ID + // that the bot definitely does not own; Discord may also return 404. + await runEffect( + bulkSetApplicationCommands({ + application_id: NON_EXISTENT_APPLICATION_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/bulkSetGuildApplicationCommands.test.ts b/packages/discord/test/bulkSetGuildApplicationCommands.test.ts new file mode 100644 index 000000000..1c2981e7d --- /dev/null +++ b/packages/discord/test/bulkSetGuildApplicationCommands.test.ts @@ -0,0 +1,131 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { bulkSetGuildApplicationCommands } from "../src/operations/bulkSetGuildApplicationCommands.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// The endpoint requires: +// - the bot's application_id (snowflake) and a guild_id (snowflake) the +// application is installed in. +// The SDK's input schema currently only exposes the path parameters and not +// the request body. As a result the operation performs a PUT with no body, +// which Discord treats as "set commands to no commands" — a destructive +// guild-commands wipe. Operators must opt in with +// DISCORD_TEST_APPLICATION_ID + DISCORD_TEST_GUILD_ID + +// DISCORD_TEST_ALLOW_DESTRUCTIVE_COMMANDS=1. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const ALLOW_DESTRUCTIVE = + process.env.DISCORD_TEST_ALLOW_DESTRUCTIVE_COMMANDS === "1"; + +// Snowflake-format identifiers that should not match real Discord resources. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; +const NON_EXISTENT_GUILD_ID = "100000000000000001"; + +describe("bulkSetGuildApplicationCommands", () => { + it("happy path - bulk-sets the application's guild-scoped commands", async () => { + if (!TEST_APPLICATION_ID || !TEST_GUILD_ID || !ALLOW_DESTRUCTIVE) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID, DISCORD_TEST_GUILD_ID and DISCORD_TEST_ALLOW_DESTRUCTIVE_COMMANDS=1 are required for the bulkSetGuildApplicationCommands happy path (this PUT replaces ALL guild commands for the application).", + ); + } + await runEffect( + bulkSetGuildApplicationCommands({ + application_id: TEST_APPLICATION_ID, + guild_id: TEST_GUILD_ID, + }).pipe( + Effect.tap((commands) => + Effect.sync(() => { + expect(Array.isArray(commands)).toBe(true); + for (const c of commands) { + expect(typeof c.id).toBe("string"); + expect(typeof c.application_id).toBe("string"); + expect(typeof c.name).toBe("string"); + expect(typeof c.description).toBe("string"); + expect(c.application_id).toBe(TEST_APPLICATION_ID); + } + }), + ), + ), + ); + }); + + it("error - NotFound for non-existent application_id", async () => { + await runEffect( + bulkSetGuildApplicationCommands({ + application_id: NON_EXISTENT_APPLICATION_ID, + guild_id: TEST_GUILD_ID ?? NON_EXISTENT_GUILD_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for an unseen application_id, but may + // surface as Forbidden when the bot's token does not own it, or + // BadRequest if the route rejects the empty body. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) application_id", async () => { + await runEffect( + bulkSetGuildApplicationCommands({ + application_id: "not-a-snowflake", + guild_id: TEST_GUILD_ID ?? NON_EXISTENT_GUILD_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord rejects malformed snowflakes with 400 Invalid Form Body; + // routing layers may also classify it as 404, or the bot may lack + // ownership and receive 403. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when the application is not installed in the guild", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the Forbidden test", + ); + } + // Targeting a guild_id where the application has not been authorized + // typically yields Forbidden (50001 Missing Access). May also surface as + // NotFound for an unseen guild, or BadRequest from form validation. + await runEffect( + bulkSetGuildApplicationCommands({ + application_id: TEST_APPLICATION_ID, + guild_id: NON_EXISTENT_GUILD_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/bulkUpdateGuildChannels.test.ts b/packages/discord/test/bulkUpdateGuildChannels.test.ts new file mode 100644 index 000000000..c24ddc900 --- /dev/null +++ b/packages/discord/test/bulkUpdateGuildChannels.test.ts @@ -0,0 +1,105 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { bulkUpdateGuildChannels } from "../src/operations/bulkUpdateGuildChannels.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// The endpoint requires: +// - a guild the bot is in with MANAGE_CHANNELS permission. +// The SDK's input schema currently only exposes guild_id and not the body +// (an array of {id, position?, lock_permissions?, parent_id?}). With no +// body, Discord interprets the call as a no-op reorder — i.e. update zero +// channels. That is safe to invoke against a test guild. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifier that should not match a real guild. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; + +describe("bulkUpdateGuildChannels", () => { + it("happy path - bulk-updates guild channel positions (no-op body)", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the bulkUpdateGuildChannels happy path", + ); + } + await runEffect( + bulkUpdateGuildChannels({ + guild_id: TEST_GUILD_ID, + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + // Discord returns 204 No Content; the SDK decodes to void / + // undefined. + expect(result).toBeUndefined(); + }), + ), + ), + ); + }); + + it("error - NotFound for non-existent guild_id", async () => { + await runEffect( + bulkUpdateGuildChannels({ + guild_id: NON_EXISTENT_GUILD_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unseen guilds, but may surface as + // Forbidden (50001 Missing Access) when the bot can't see the guild. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) guild_id", async () => { + await runEffect( + bulkUpdateGuildChannels({ + guild_id: "not-a-snowflake", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord rejects malformed snowflakes with 400 Invalid Form Body; + // routing layers may also classify it as 404. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + bulkUpdateGuildChannels({ + guild_id: NON_EXISTENT_GUILD_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/bulkUpdateGuildRoles.test.ts b/packages/discord/test/bulkUpdateGuildRoles.test.ts new file mode 100644 index 000000000..27d7ce479 --- /dev/null +++ b/packages/discord/test/bulkUpdateGuildRoles.test.ts @@ -0,0 +1,113 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { bulkUpdateGuildRoles } from "../src/operations/bulkUpdateGuildRoles.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// The endpoint requires: +// - a guild the bot is in with MANAGE_ROLES permission. +// The SDK's input schema currently only exposes guild_id and not the body +// (an array of {id, position?}). With no body, Discord interprets the call +// as a no-op reorder and returns the current role list. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifier that should not match a real guild. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; + +describe("bulkUpdateGuildRoles", () => { + it("happy path - bulk-updates guild role positions (no-op body)", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the bulkUpdateGuildRoles happy path", + ); + } + await runEffect( + bulkUpdateGuildRoles({ + guild_id: TEST_GUILD_ID, + }).pipe( + Effect.tap((roles) => + Effect.sync(() => { + // Discord returns the array of guild roles after the (no-op) update. + expect(Array.isArray(roles)).toBe(true); + // Every guild has at least the @everyone role (id == guild_id). + expect(roles.length).toBeGreaterThan(0); + for (const r of roles) { + expect(typeof r.id).toBe("string"); + expect(typeof r.name).toBe("string"); + expect(typeof r.permissions).toBe("string"); + expect(typeof r.position).toBe("number"); + expect(typeof r.color).toBe("number"); + expect(typeof r.flags).toBe("number"); + } + }), + ), + ), + ); + }); + + it("error - NotFound for non-existent guild_id", async () => { + await runEffect( + bulkUpdateGuildRoles({ + guild_id: NON_EXISTENT_GUILD_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unseen guilds, but may surface as + // Forbidden (50001 Missing Access) when the bot can't see the guild. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) guild_id", async () => { + await runEffect( + bulkUpdateGuildRoles({ + guild_id: "not-a-snowflake", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord rejects malformed snowflakes with 400 Invalid Form Body; + // routing layers may also classify it as 404. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + bulkUpdateGuildRoles({ + guild_id: NON_EXISTENT_GUILD_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/bulkUpdateLobbyMembers.test.ts b/packages/discord/test/bulkUpdateLobbyMembers.test.ts new file mode 100644 index 000000000..71dc94dd8 --- /dev/null +++ b/packages/discord/test/bulkUpdateLobbyMembers.test.ts @@ -0,0 +1,113 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { bulkUpdateLobbyMembers } from "../src/operations/bulkUpdateLobbyMembers.ts"; +import { createLobby } from "../src/operations/createLobby.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint requires: +// - the application owns the lobby_id (Discord lobbies are scoped to the +// application that created them). +// The SDK's input schema currently only exposes lobby_id and not the body +// (an array of {id, metadata?, flags?}). With no body, Discord interprets +// the call as a no-op bulk update and returns the current lobby members. +// Lobbies auto-expire via idle_timeout_seconds, so no explicit teardown is +// required. + +// Snowflake-format identifier that should not match a real lobby. +const NON_EXISTENT_LOBBY_ID = "100000000000000000"; + +describe("bulkUpdateLobbyMembers", () => { + it("happy path - bulk-updates lobby members (no-op body) and returns current members", async () => { + await runEffect( + Effect.gen(function* () { + const lobby = yield* createLobby({ + idle_timeout_seconds: 60, + metadata: { + test_run_id: testRunId, + purpose: "bulkUpdateLobbyMembers-test", + }, + }); + const result = yield* bulkUpdateLobbyMembers({ + lobby_id: lobby.id, + }); + // Discord returns the array of lobby members after the (no-op) bulk + // update. A freshly created lobby with no members yields an empty + // array, but the shape is still an Array. + expect(Array.isArray(result)).toBe(true); + for (const m of result) { + expect(typeof m.id).toBe("string"); + expect(typeof m.flags).toBe("number"); + } + }), + ); + }); + + it("error - NotFound for non-existent lobby_id", async () => { + await runEffect( + bulkUpdateLobbyMembers({ + lobby_id: NON_EXISTENT_LOBBY_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for an unseen lobby but may surface as + // Forbidden when the application can't access the lobby. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) lobby_id", async () => { + await runEffect( + bulkUpdateLobbyMembers({ + lobby_id: "not-a-snowflake", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord rejects malformed snowflakes with 400 Invalid Form Body; + // routing layers may also classify it as 404. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for lobby owned by a different application", async () => { + // Discord returns Forbidden (50001 Missing Access) for lobbies the + // application does not own; for fully unknown snowflakes it may surface + // as NotFound instead. + await runEffect( + bulkUpdateLobbyMembers({ + lobby_id: NON_EXISTENT_LOBBY_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/consumeEntitlement.test.ts b/packages/discord/test/consumeEntitlement.test.ts new file mode 100644 index 000000000..5cbb904bc --- /dev/null +++ b/packages/discord/test/consumeEntitlement.test.ts @@ -0,0 +1,122 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { consumeEntitlement } from "../src/operations/consumeEntitlement.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// The endpoint requires: +// - the bot's application_id (snowflake). +// - an entitlement_id for a CONSUMABLE one-time purchase entitlement that +// has not yet been consumed. Consumption is irreversible — Discord marks +// the entitlement as consumed and a future purchase / grant is needed +// to test consumption again. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; +const TEST_ENTITLEMENT_ID = process.env.DISCORD_TEST_CONSUMABLE_ENTITLEMENT_ID; + +// Snowflake-format identifiers that should not match real Discord resources. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; +const NON_EXISTENT_ENTITLEMENT_ID = "100000000000000001"; + +describe("consumeEntitlement", () => { + it("happy path - consumes a one-time consumable entitlement", async () => { + if (!TEST_APPLICATION_ID || !TEST_ENTITLEMENT_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID and DISCORD_TEST_CONSUMABLE_ENTITLEMENT_ID env vars are required for the consumeEntitlement happy path (the entitlement must be a CONSUMABLE one-time purchase that has not been consumed yet — consumption is irreversible).", + ); + } + await runEffect( + consumeEntitlement({ + application_id: TEST_APPLICATION_ID, + entitlement_id: TEST_ENTITLEMENT_ID, + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + // Discord returns 204 No Content on success; the SDK decodes it + // to void / undefined. + expect(result).toBeUndefined(); + }), + ), + ), + ); + }); + + it("error - NotFound for non-existent entitlement_id", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the NotFound test", + ); + } + await runEffect( + consumeEntitlement({ + application_id: TEST_APPLICATION_ID, + entitlement_id: NON_EXISTENT_ENTITLEMENT_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound (error code 10070) for an entitlement + // that does not exist; may surface as Forbidden if the application + // does not own the entitlement. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) entitlement_id", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the BadRequest test", + ); + } + await runEffect( + consumeEntitlement({ + application_id: TEST_APPLICATION_ID, + entitlement_id: "not-a-snowflake", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord rejects malformed snowflakes with 400 Invalid Form Body; + // routing layers may also classify it as 404. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for application_id the bot does not own", async () => { + // Calling against an application_id the bot's token does not own + // typically yields 403 Forbidden; may also surface as 404 NotFound when + // the route resolves the application before the permission check. + await runEffect( + consumeEntitlement({ + application_id: NON_EXISTENT_APPLICATION_ID, + entitlement_id: NON_EXISTENT_ENTITLEMENT_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createApplicationCommand.test.ts b/packages/discord/test/createApplicationCommand.test.ts new file mode 100644 index 000000000..3697649bf --- /dev/null +++ b/packages/discord/test/createApplicationCommand.test.ts @@ -0,0 +1,130 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createApplicationCommand } from "../src/operations/createApplicationCommand.ts"; +import { deleteApplicationCommand } from "../src/operations/deleteApplicationCommand.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint requires: +// - the bot's application_id (snowflake) — the bot's token must own it. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; + +// Snowflake-format identifier that should not match a real application. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; + +// Discord requires CHAT_INPUT command names to match ^[-_\p{L}\p{N}]{1,32}$. +const cmdName = (suffix: string): string => + `dtest-${suffix}-${testRunId}`.toLowerCase().slice(0, 32); + +describe("createApplicationCommand", () => { + it("happy path - creates an application command and deletes it on cleanup", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the createApplicationCommand happy path", + ); + } + const name = cmdName("happy"); + await runEffect( + Effect.gen(function* () { + const cmd = yield* createApplicationCommand({ + application_id: TEST_APPLICATION_ID, + name, + description: "distilled test command", + }); + return yield* Effect.sync(() => { + expect(typeof cmd.id).toBe("string"); + expect(cmd.application_id).toBe(TEST_APPLICATION_ID); + expect(cmd.name).toBe(name); + expect(cmd.description).toBe("distilled test command"); + }).pipe( + Effect.ensuring( + deleteApplicationCommand({ + application_id: TEST_APPLICATION_ID, + command_id: cmd.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent application_id", async () => { + await runEffect( + createApplicationCommand({ + application_id: NON_EXISTENT_APPLICATION_ID, + name: cmdName("nf"), + description: "distilled test", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for an unseen application_id, but may + // surface as Forbidden when the bot's token does not own it. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for invalid command name (uppercase + spaces)", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the BadRequest test", + ); + } + // Discord's CHAT_INPUT command names must match ^[-_\p{L}\p{N}]{1,32}$ — + // uppercase letters and spaces are explicitly rejected with 400 Invalid + // Form Body. + await runEffect( + createApplicationCommand({ + application_id: TEST_APPLICATION_ID, + name: "INVALID NAME WITH SPACES", + description: "distilled test", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for application_id the bot does not own", async () => { + // Calling against an application_id the bot's token does not own + // typically yields 403 Forbidden; may also surface as 404 NotFound when + // the route resolves the application before the permission check. + await runEffect( + createApplicationCommand({ + application_id: NON_EXISTENT_APPLICATION_ID, + name: cmdName("fb"), + description: "distilled test", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createApplicationEmoji.test.ts b/packages/discord/test/createApplicationEmoji.test.ts new file mode 100644 index 000000000..8f3482dbc --- /dev/null +++ b/packages/discord/test/createApplicationEmoji.test.ts @@ -0,0 +1,140 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createApplicationEmoji } from "../src/operations/createApplicationEmoji.ts"; +import { deleteApplicationEmoji } from "../src/operations/deleteApplicationEmoji.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Smallest valid 1x1 transparent PNG, encoded as a data URI. Discord accepts +// data URIs of the form "data:image/{png,jpeg,gif};base64,...". +const TINY_PNG_DATA_URI = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII="; + +// The endpoint requires: +// - the bot's application_id (snowflake) — the bot's token must own it. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; + +// Snowflake-format identifier that should not match a real application. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; + +// Discord requires emoji names to match ^[a-zA-Z0-9_]{2,32}$. +const emojiName = (suffix: string): string => { + // testRunId is 8 hex chars; suffix kept short to fit 32-char limit. + const raw = `dt_${suffix}_${testRunId}`; + return raw.replace(/[^a-zA-Z0-9_]/g, "_").slice(0, 32); +}; + +describe("createApplicationEmoji", () => { + it("happy path - creates an application emoji and deletes it on cleanup", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the createApplicationEmoji happy path", + ); + } + const name = emojiName("happy"); + await runEffect( + Effect.gen(function* () { + const emoji = yield* createApplicationEmoji({ + application_id: TEST_APPLICATION_ID, + name, + image: TINY_PNG_DATA_URI, + }); + return yield* Effect.sync(() => { + expect(typeof emoji.id).toBe("string"); + expect(emoji.name).toBe(name); + expect(Array.isArray(emoji.roles)).toBe(true); + expect(typeof emoji.require_colons).toBe("boolean"); + expect(typeof emoji.managed).toBe("boolean"); + expect(typeof emoji.animated).toBe("boolean"); + expect(typeof emoji.available).toBe("boolean"); + }).pipe( + Effect.ensuring( + deleteApplicationEmoji({ + application_id: TEST_APPLICATION_ID, + emoji_id: emoji.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent application_id", async () => { + await runEffect( + createApplicationEmoji({ + application_id: NON_EXISTENT_APPLICATION_ID, + name: emojiName("nf"), + image: TINY_PNG_DATA_URI, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for an unseen application_id, but may + // surface as Forbidden when the bot's token does not own it. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for invalid emoji name (contains hyphens)", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the BadRequest test", + ); + } + // Discord's emoji names must match ^[a-zA-Z0-9_]{2,32}$ — hyphens and + // spaces are rejected with 400 Invalid Form Body. + await runEffect( + createApplicationEmoji({ + application_id: TEST_APPLICATION_ID, + name: "bad-name with spaces", + image: TINY_PNG_DATA_URI, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for application_id the bot does not own", async () => { + // Calling against an application_id the bot's token does not own + // typically yields 403 Forbidden; may also surface as 404 NotFound when + // the route resolves the application before the permission check. + await runEffect( + createApplicationEmoji({ + application_id: NON_EXISTENT_APPLICATION_ID, + name: emojiName("fb"), + image: TINY_PNG_DATA_URI, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createAutoModerationRule.test.ts b/packages/discord/test/createAutoModerationRule.test.ts new file mode 100644 index 000000000..14ee808b3 --- /dev/null +++ b/packages/discord/test/createAutoModerationRule.test.ts @@ -0,0 +1,126 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createAutoModerationRule } from "../src/operations/createAutoModerationRule.ts"; +import { deleteAutoModerationRule } from "../src/operations/deleteAutoModerationRule.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// The endpoint requires: +// - a guild the bot is in with MANAGE_GUILD permission. +// The SDK's input schema currently only exposes guild_id and not the body +// (name, event_type, trigger_type, actions, etc.). Without those required +// fields the API call sends an empty body, which Discord rejects with 400 +// Invalid Form Body. Until the spec is patched to surface the body schema, +// the happy path is exercised end-to-end against a real guild via the +// gated env vars below; if the SDK truly sends no body Discord will reject +// it and the assertion will surface the failure. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifier that should not match a real guild. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; + +describe("createAutoModerationRule", () => { + it("happy path - creates an auto-moderation rule and deletes it on cleanup", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the createAutoModerationRule happy path", + ); + } + await runEffect( + Effect.gen(function* () { + // The output is typed as an opaque value because the spec does not + // describe the response body. Cast for assertions. + const ruleRaw = yield* createAutoModerationRule({ + guild_id: TEST_GUILD_ID, + }); + const rule = ruleRaw as { id?: string; guild_id?: string }; + return yield* Effect.sync(() => { + expect(typeof rule).toBe("object"); + expect(typeof rule.id).toBe("string"); + if (rule.guild_id !== undefined) { + expect(rule.guild_id).toBe(TEST_GUILD_ID); + } + }).pipe( + Effect.ensuring( + rule.id + ? deleteAutoModerationRule({ + guild_id: TEST_GUILD_ID, + rule_id: rule.id, + }).pipe(Effect.ignore) + : Effect.void, + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent guild_id", async () => { + await runEffect( + createAutoModerationRule({ + guild_id: NON_EXISTENT_GUILD_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unseen guilds, but may surface as + // Forbidden (50001 Missing Access) when the bot can't see the guild. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) guild_id", async () => { + await runEffect( + createAutoModerationRule({ + guild_id: "not-a-snowflake", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord rejects malformed snowflakes with 400 Invalid Form Body; + // routing layers may also classify it as 404, or the bot may lack + // access and receive 403. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + createAutoModerationRule({ + guild_id: NON_EXISTENT_GUILD_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createChannelInvite.test.ts b/packages/discord/test/createChannelInvite.test.ts new file mode 100644 index 000000000..e01881a0c --- /dev/null +++ b/packages/discord/test/createChannelInvite.test.ts @@ -0,0 +1,114 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createChannelInvite } from "../src/operations/createChannelInvite.ts"; +import { inviteRevoke } from "../src/operations/inviteRevoke.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// The endpoint requires: +// - a guild text/voice channel the bot is in with CREATE_INSTANT_INVITE +// permission. With no body, Discord creates a default 1-day invite. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifier that should not match a real channel. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; + +describe("createChannelInvite", () => { + it("happy path - creates a channel invite and revokes it on cleanup", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the createChannelInvite happy path", + ); + } + await runEffect( + Effect.gen(function* () { + // Output is typed as an opaque value because the spec does not + // describe the response body. Cast for assertions. + const inviteRaw = yield* createChannelInvite({ + channel_id: TEST_CHANNEL_ID, + }); + const invite = inviteRaw as { code?: string; channel?: { id?: string } }; + return yield* Effect.sync(() => { + expect(typeof invite).toBe("object"); + expect(typeof invite.code).toBe("string"); + }).pipe( + Effect.ensuring( + invite.code + ? inviteRevoke({ code: invite.code }).pipe(Effect.ignore) + : Effect.void, + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent channel_id", async () => { + await runEffect( + createChannelInvite({ + channel_id: NON_EXISTENT_CHANNEL_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unseen channels, but may surface as + // Forbidden (50001 Missing Access) when the bot can't see it. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) channel_id", async () => { + await runEffect( + createChannelInvite({ + channel_id: "not-a-snowflake", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord rejects malformed snowflakes with 400 Invalid Form Body; + // routing layers may also classify it as 404, or the bot may lack + // access and receive 403. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for a channel the bot lacks CREATE_INSTANT_INVITE on", async () => { + // Calling against a snowflake-shaped channel_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the channel before the permission check. + await runEffect( + createChannelInvite({ + channel_id: NON_EXISTENT_CHANNEL_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createDm.test.ts b/packages/discord/test/createDm.test.ts new file mode 100644 index 000000000..3125b4443 --- /dev/null +++ b/packages/discord/test/createDm.test.ts @@ -0,0 +1,114 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createDm } from "../src/operations/createDm.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// The endpoint requires: +// - a recipient_id (snowflake) for a real Discord user that the bot can +// DM (typically a user that shares at least one mutual guild and has +// "Allow direct messages from server members" enabled). +const TEST_USER_ID = process.env.DISCORD_TEST_USER_ID; + +// Snowflake-format identifier that should not match a real user. +const NON_EXISTENT_USER_ID = "100000000000000000"; + +describe("createDm", () => { + it("happy path - creates (or returns existing) DM channel with a user", async () => { + if (!TEST_USER_ID) { + throw new Error( + "DISCORD_TEST_USER_ID env var is required for the createDm happy path", + ); + } + await runEffect( + createDm({ + recipient_id: TEST_USER_ID, + }).pipe( + Effect.tap((channelRaw) => + Effect.sync(() => { + // Output is typed as an opaque value because the spec does not + // describe the response body. Cast for assertions. + const channel = channelRaw as { id?: string; type?: number }; + expect(typeof channel).toBe("object"); + expect(typeof channel.id).toBe("string"); + // Discord type 1 == DM channel. + if (channel.type !== undefined) { + expect(typeof channel.type).toBe("number"); + } + }), + ), + ), + ); + }); + + it("error - NotFound for non-existent recipient_id", async () => { + await runEffect( + createDm({ + recipient_id: NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for an unseen recipient_id (10013) but + // may surface as BadRequest if validation runs first. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "BadRequest", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) recipient_id", async () => { + await runEffect( + createDm({ + recipient_id: "not-a-snowflake", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord rejects malformed snowflakes with 400 Invalid Form Body; + // routing layers may also classify it as 404. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when DMing the bot's own user_id", async () => { + // Discord disallows self-DMs: the bot cannot create a DM channel with + // its own user_id and typically returns Forbidden (50007 Cannot send + // messages to this user) or BadRequest. We use a snowflake the bot does + // not control to ensure the failure path; for an arbitrary user that has + // disabled DMs this also yields Forbidden. + await runEffect( + createDm({ + recipient_id: NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createEntitlement.test.ts b/packages/discord/test/createEntitlement.test.ts new file mode 100644 index 000000000..b2a3067df --- /dev/null +++ b/packages/discord/test/createEntitlement.test.ts @@ -0,0 +1,143 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createEntitlement } from "../src/operations/createEntitlement.ts"; +import { deleteEntitlement } from "../src/operations/deleteEntitlement.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// The endpoint requires: +// - the bot's application_id (snowflake) — the bot's token must own it. +// - a sku_id (snowflake) belonging to that application. +// - an owner_id (snowflake) — guild_id when owner_type=1, user_id when 2. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; +const TEST_SKU_ID = process.env.DISCORD_TEST_SKU_ID; +const TEST_OWNER_ID = process.env.DISCORD_TEST_USER_ID; + +// Snowflake-format identifiers that should not match real Discord resources. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; +const NON_EXISTENT_SKU_ID = "100000000000000001"; +const NON_EXISTENT_OWNER_ID = "100000000000000002"; + +// Discord owner types: 1 = guild subscription, 2 = user subscription. +const OWNER_TYPE_USER = 2; + +describe("createEntitlement", () => { + it("happy path - creates a test entitlement and deletes it on cleanup", async () => { + if (!TEST_APPLICATION_ID || !TEST_SKU_ID || !TEST_OWNER_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID, DISCORD_TEST_SKU_ID and DISCORD_TEST_USER_ID env vars are required for the createEntitlement happy path", + ); + } + await runEffect( + Effect.gen(function* () { + const entitlement = yield* createEntitlement({ + application_id: TEST_APPLICATION_ID, + sku_id: TEST_SKU_ID, + owner_id: TEST_OWNER_ID, + owner_type: OWNER_TYPE_USER, + }); + return yield* Effect.sync(() => { + expect(typeof entitlement.id).toBe("string"); + expect(entitlement.application_id).toBe(TEST_APPLICATION_ID); + expect(entitlement.sku_id).toBe(TEST_SKU_ID); + expect(typeof entitlement.deleted).toBe("boolean"); + }).pipe( + Effect.ensuring( + deleteEntitlement({ + application_id: TEST_APPLICATION_ID, + entitlement_id: entitlement.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent sku_id", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the NotFound test", + ); + } + await runEffect( + createEntitlement({ + application_id: TEST_APPLICATION_ID, + sku_id: NON_EXISTENT_SKU_ID, + owner_id: TEST_OWNER_ID ?? NON_EXISTENT_OWNER_ID, + owner_type: OWNER_TYPE_USER, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound (10068) for an sku_id that does not + // belong to the application; may surface as Forbidden or BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "BadRequest", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - BadRequest for invalid owner_type (out of range)", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the BadRequest test", + ); + } + // Discord's owner_type must be 1 or 2; any other value is rejected with + // 400 Invalid Form Body. May also surface as Forbidden or NotFound. + await runEffect( + createEntitlement({ + application_id: TEST_APPLICATION_ID, + sku_id: TEST_SKU_ID ?? NON_EXISTENT_SKU_ID, + owner_id: TEST_OWNER_ID ?? NON_EXISTENT_OWNER_ID, + owner_type: 99, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for application_id the bot does not own", async () => { + // Calling against an application_id the bot's token does not own + // typically yields 403 Forbidden; may also surface as 404 NotFound when + // the route resolves the application before the permission check. + await runEffect( + createEntitlement({ + application_id: NON_EXISTENT_APPLICATION_ID, + sku_id: TEST_SKU_ID ?? NON_EXISTENT_SKU_ID, + owner_id: TEST_OWNER_ID ?? NON_EXISTENT_OWNER_ID, + owner_type: OWNER_TYPE_USER, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createGuildApplicationCommand.test.ts b/packages/discord/test/createGuildApplicationCommand.test.ts new file mode 100644 index 000000000..bcf8c8c5e --- /dev/null +++ b/packages/discord/test/createGuildApplicationCommand.test.ts @@ -0,0 +1,149 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildApplicationCommand } from "../src/operations/createGuildApplicationCommand.ts"; +import { deleteGuildApplicationCommand } from "../src/operations/deleteGuildApplicationCommand.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint requires: +// - the bot's application_id (snowflake) — the bot's token must own it. +// - a guild_id (snowflake) where the application is installed. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifiers that should not match real Discord resources. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; +const NON_EXISTENT_GUILD_ID = "100000000000000001"; + +// Discord requires CHAT_INPUT command names to match ^[-_\p{L}\p{N}]{1,32}$. +const cmdName = (suffix: string): string => + `dtest-${suffix}-${testRunId}`.toLowerCase().slice(0, 32); + +describe("createGuildApplicationCommand", () => { + it("happy path - creates a guild application command and deletes it on cleanup", async () => { + if (!TEST_APPLICATION_ID || !TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID and DISCORD_TEST_GUILD_ID env vars are required for the createGuildApplicationCommand happy path", + ); + } + const name = cmdName("happy"); + await runEffect( + Effect.gen(function* () { + const cmd = yield* createGuildApplicationCommand({ + application_id: TEST_APPLICATION_ID, + guild_id: TEST_GUILD_ID, + name, + description: "distilled test guild command", + }); + return yield* Effect.sync(() => { + expect(typeof cmd.id).toBe("string"); + expect(cmd.application_id).toBe(TEST_APPLICATION_ID); + expect(cmd.name).toBe(name); + expect(cmd.description).toBe("distilled test guild command"); + if (cmd.guild_id !== undefined) { + expect(cmd.guild_id).toBe(TEST_GUILD_ID); + } + }).pipe( + Effect.ensuring( + deleteGuildApplicationCommand({ + application_id: TEST_APPLICATION_ID, + guild_id: TEST_GUILD_ID, + command_id: cmd.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent application_id", async () => { + await runEffect( + createGuildApplicationCommand({ + application_id: NON_EXISTENT_APPLICATION_ID, + guild_id: TEST_GUILD_ID ?? NON_EXISTENT_GUILD_ID, + name: cmdName("nf"), + description: "distilled test", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for an unseen application_id, but may + // surface as Forbidden when the bot's token does not own it. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - BadRequest for invalid command name (uppercase + spaces)", async () => { + if (!TEST_APPLICATION_ID || !TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID and DISCORD_TEST_GUILD_ID env vars are required for the BadRequest test", + ); + } + // Discord's CHAT_INPUT command names must match ^[-_\p{L}\p{N}]{1,32}$ — + // uppercase letters and spaces are explicitly rejected with 400 Invalid + // Form Body. + await runEffect( + createGuildApplicationCommand({ + application_id: TEST_APPLICATION_ID, + guild_id: TEST_GUILD_ID, + name: "INVALID NAME WITH SPACES", + description: "distilled test", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when the application is not installed in the guild", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the Forbidden test", + ); + } + // Targeting a guild_id where the application has not been authorized + // typically yields Forbidden (50001 Missing Access). May also surface + // as NotFound for an unseen guild, or BadRequest from form validation. + await runEffect( + createGuildApplicationCommand({ + application_id: TEST_APPLICATION_ID, + guild_id: NON_EXISTENT_GUILD_ID, + name: cmdName("fb"), + description: "distilled test", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createGuildChannel.test.ts b/packages/discord/test/createGuildChannel.test.ts new file mode 100644 index 000000000..c596c58d5 --- /dev/null +++ b/packages/discord/test/createGuildChannel.test.ts @@ -0,0 +1,133 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildChannel } from "../src/operations/createGuildChannel.ts"; +import { deleteChannel } from "../src/operations/deleteChannel.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint requires: +// - a guild the bot is in with MANAGE_CHANNELS permission. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifier that should not match a real guild. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; + +// Discord channel name: 1..100 chars; we keep it shorter for safety. +const channelName = (suffix: string): string => + `dtest-${suffix}-${testRunId}`.slice(0, 100); + +describe("createGuildChannel", () => { + it("happy path - creates a guild text channel and deletes it on cleanup", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the createGuildChannel happy path", + ); + } + const name = channelName("happy"); + await runEffect( + Effect.gen(function* () { + // Discord type 0 == GUILD_TEXT. + const channel = yield* createGuildChannel({ + guild_id: TEST_GUILD_ID, + name, + type: 0, + topic: "distilled test channel", + }); + return yield* Effect.sync(() => { + expect(typeof channel.id).toBe("string"); + expect(channel.guild_id).toBe(TEST_GUILD_ID); + expect(channel.name).toBe(name); + expect(typeof channel.position).toBe("number"); + expect(typeof channel.flags).toBe("number"); + }).pipe( + Effect.ensuring( + deleteChannel({ channel_id: channel.id }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent guild_id", async () => { + await runEffect( + createGuildChannel({ + guild_id: NON_EXISTENT_GUILD_ID, + name: channelName("nf"), + type: 0, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unseen guilds, but may surface as + // Forbidden (50001 Missing Access) when the bot can't see it. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - BadRequest for empty channel name", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the BadRequest test", + ); + } + // Discord rejects empty channel names with 400 Invalid Form Body. May + // also surface as Forbidden if MANAGE_CHANNELS validation fires first, + // or NotFound for an unseen guild. + await runEffect( + createGuildChannel({ + guild_id: TEST_GUILD_ID, + name: "", + type: 0, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + createGuildChannel({ + guild_id: NON_EXISTENT_GUILD_ID, + name: channelName("fb"), + type: 0, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createGuildEmoji.test.ts b/packages/discord/test/createGuildEmoji.test.ts new file mode 100644 index 000000000..21cc3f470 --- /dev/null +++ b/packages/discord/test/createGuildEmoji.test.ts @@ -0,0 +1,142 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildEmoji } from "../src/operations/createGuildEmoji.ts"; +import { deleteGuildEmoji } from "../src/operations/deleteGuildEmoji.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Smallest valid 1x1 transparent PNG, encoded as a data URI. Discord accepts +// data URIs of the form "data:image/{png,jpeg,gif};base64,...". +const TINY_PNG_DATA_URI = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII="; + +// The endpoint requires: +// - a guild the bot is in with CREATE_GUILD_EXPRESSIONS permission. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifier that should not match a real guild. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; + +// Discord requires emoji names to match ^[a-zA-Z0-9_]{2,32}$. +const emojiName = (suffix: string): string => { + const raw = `dt_${suffix}_${testRunId}`; + return raw.replace(/[^a-zA-Z0-9_]/g, "_").slice(0, 32); +}; + +describe("createGuildEmoji", () => { + it("happy path - creates a guild emoji and deletes it on cleanup", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the createGuildEmoji happy path", + ); + } + const name = emojiName("happy"); + await runEffect( + Effect.gen(function* () { + const emoji = yield* createGuildEmoji({ + guild_id: TEST_GUILD_ID, + name, + image: TINY_PNG_DATA_URI, + }); + return yield* Effect.sync(() => { + expect(typeof emoji.id).toBe("string"); + expect(emoji.name).toBe(name); + expect(Array.isArray(emoji.roles)).toBe(true); + expect(typeof emoji.require_colons).toBe("boolean"); + expect(typeof emoji.managed).toBe("boolean"); + expect(typeof emoji.animated).toBe("boolean"); + expect(typeof emoji.available).toBe("boolean"); + }).pipe( + Effect.ensuring( + deleteGuildEmoji({ + guild_id: TEST_GUILD_ID, + emoji_id: emoji.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent guild_id", async () => { + await runEffect( + createGuildEmoji({ + guild_id: NON_EXISTENT_GUILD_ID, + name: emojiName("nf"), + image: TINY_PNG_DATA_URI, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unseen guilds, but may surface as + // Forbidden (50001 Missing Access) when the bot can't see it. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - BadRequest for invalid emoji name (contains hyphens)", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the BadRequest test", + ); + } + // Discord's emoji names must match ^[a-zA-Z0-9_]{2,32}$ — hyphens and + // spaces are rejected with 400 Invalid Form Body. + await runEffect( + createGuildEmoji({ + guild_id: TEST_GUILD_ID, + name: "bad-name with spaces", + image: TINY_PNG_DATA_URI, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + createGuildEmoji({ + guild_id: NON_EXISTENT_GUILD_ID, + name: emojiName("fb"), + image: TINY_PNG_DATA_URI, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createGuildRole.test.ts b/packages/discord/test/createGuildRole.test.ts new file mode 100644 index 000000000..67c0c708d --- /dev/null +++ b/packages/discord/test/createGuildRole.test.ts @@ -0,0 +1,137 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildRole } from "../src/operations/createGuildRole.ts"; +import { deleteGuildRole } from "../src/operations/deleteGuildRole.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint requires: +// - a guild the bot is in with MANAGE_ROLES permission. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifier that should not match a real guild. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; + +// Discord role names: 1..100 chars; we keep it short. +const roleName = (suffix: string): string => + `dtest-${suffix}-${testRunId}`.slice(0, 100); + +describe("createGuildRole", () => { + it("happy path - creates a guild role and deletes it on cleanup", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the createGuildRole happy path", + ); + } + const name = roleName("happy"); + await runEffect( + Effect.gen(function* () { + const role = yield* createGuildRole({ + guild_id: TEST_GUILD_ID, + name, + mentionable: false, + hoist: false, + }); + return yield* Effect.sync(() => { + expect(typeof role.id).toBe("string"); + expect(role.name).toBe(name); + expect(typeof role.permissions).toBe("string"); + expect(typeof role.position).toBe("number"); + expect(typeof role.color).toBe("number"); + expect(typeof role.flags).toBe("number"); + expect(role.mentionable).toBe(false); + expect(role.hoist).toBe(false); + }).pipe( + Effect.ensuring( + deleteGuildRole({ + guild_id: TEST_GUILD_ID, + role_id: role.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent guild_id", async () => { + await runEffect( + createGuildRole({ + guild_id: NON_EXISTENT_GUILD_ID, + name: roleName("nf"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unseen guilds, but may surface as + // Forbidden (50001 Missing Access) when the bot can't see it. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - BadRequest for permissions value out of bitfield range", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the BadRequest test", + ); + } + // Discord's permissions field is a 64-bit integer bitfield. Negative + // values are rejected with 400 Invalid Form Body. May also surface as + // Forbidden if MANAGE_ROLES validation fires first, or NotFound for + // an unseen guild. + await runEffect( + createGuildRole({ + guild_id: TEST_GUILD_ID, + name: roleName("bad"), + permissions: -1, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + createGuildRole({ + guild_id: NON_EXISTENT_GUILD_ID, + name: roleName("fb"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createGuildScheduledEvent.test.ts b/packages/discord/test/createGuildScheduledEvent.test.ts new file mode 100644 index 000000000..b04dff2b3 --- /dev/null +++ b/packages/discord/test/createGuildScheduledEvent.test.ts @@ -0,0 +1,126 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildScheduledEvent } from "../src/operations/createGuildScheduledEvent.ts"; +import { deleteGuildScheduledEvent } from "../src/operations/deleteGuildScheduledEvent.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// The endpoint requires: +// - a guild the bot is in with MANAGE_EVENTS permission. +// The SDK's input schema currently only exposes guild_id and not the body +// (name, scheduled_start_time, entity_type, etc.). Without those required +// fields the API call sends an empty body, which Discord rejects with 400 +// Invalid Form Body. Until the spec is patched, the happy path is exercised +// end-to-end against a real guild via the gated env var below; if the SDK +// truly sends no body Discord will reject and the assertion will surface +// the failure. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifier that should not match a real guild. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; + +describe("createGuildScheduledEvent", () => { + it("happy path - creates a scheduled event and deletes it on cleanup", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the createGuildScheduledEvent happy path", + ); + } + await runEffect( + Effect.gen(function* () { + // The output is typed as an opaque value because the spec does not + // describe the response body. Cast for assertions. + const eventRaw = yield* createGuildScheduledEvent({ + guild_id: TEST_GUILD_ID, + }); + const event = eventRaw as { id?: string; guild_id?: string }; + return yield* Effect.sync(() => { + expect(typeof event).toBe("object"); + expect(typeof event.id).toBe("string"); + if (event.guild_id !== undefined) { + expect(event.guild_id).toBe(TEST_GUILD_ID); + } + }).pipe( + Effect.ensuring( + event.id + ? deleteGuildScheduledEvent({ + guild_id: TEST_GUILD_ID, + guild_scheduled_event_id: event.id, + }).pipe(Effect.ignore) + : Effect.void, + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent guild_id", async () => { + await runEffect( + createGuildScheduledEvent({ + guild_id: NON_EXISTENT_GUILD_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unseen guilds, but may surface as + // Forbidden (50001 Missing Access) when the bot can't see the guild. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) guild_id", async () => { + await runEffect( + createGuildScheduledEvent({ + guild_id: "not-a-snowflake", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord rejects malformed snowflakes with 400 Invalid Form Body; + // routing layers may also classify it as 404, or the bot may lack + // access and receive 403. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + createGuildScheduledEvent({ + guild_id: NON_EXISTENT_GUILD_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createGuildSoundboardSound.test.ts b/packages/discord/test/createGuildSoundboardSound.test.ts new file mode 100644 index 000000000..306fbb2ab --- /dev/null +++ b/packages/discord/test/createGuildSoundboardSound.test.ts @@ -0,0 +1,148 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildSoundboardSound } from "../src/operations/createGuildSoundboardSound.ts"; +import { deleteGuildSoundboardSound } from "../src/operations/deleteGuildSoundboardSound.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint requires: +// - a guild the bot is in with CREATE_GUILD_EXPRESSIONS permission and +// soundboard support (community guild or boosted). +// - a sound data URI: "data:audio/{mpeg,ogg};base64,..." up to 512KB and +// <= 5.2 seconds duration. Operators must supply their own valid clip +// via DISCORD_TEST_SOUNDBOARD_DATA_URI; no inline MP3/OGG fixture is +// small enough to embed safely. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_SOUND_DATA_URI = process.env.DISCORD_TEST_SOUNDBOARD_DATA_URI; + +// Snowflake-format identifier that should not match a real guild. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; + +// Discord requires soundboard sound names of 2..32 chars. +const soundName = (suffix: string): string => + `dt-${suffix}-${testRunId}`.slice(0, 32); + +// A clearly invalid sound payload — empty data URI — used for the BadRequest +// path; Discord rejects it with 400 Invalid Form Body. +const INVALID_SOUND_DATA_URI = "data:audio/mpeg;base64,"; + +describe("createGuildSoundboardSound", () => { + it("happy path - creates a guild soundboard sound and deletes it on cleanup", async () => { + if (!TEST_GUILD_ID || !TEST_SOUND_DATA_URI) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_SOUNDBOARD_DATA_URI env vars are required for the createGuildSoundboardSound happy path", + ); + } + const name = soundName("happy"); + await runEffect( + Effect.gen(function* () { + const sound = yield* createGuildSoundboardSound({ + guild_id: TEST_GUILD_ID, + name, + sound: TEST_SOUND_DATA_URI, + volume: 1, + }); + return yield* Effect.sync(() => { + expect(typeof sound.sound_id).toBe("string"); + expect(sound.name).toBe(name); + expect(typeof sound.volume).toBe("number"); + expect(typeof sound.available).toBe("boolean"); + if (sound.guild_id !== undefined) { + expect(sound.guild_id).toBe(TEST_GUILD_ID); + } + }).pipe( + Effect.ensuring( + deleteGuildSoundboardSound({ + guild_id: TEST_GUILD_ID, + sound_id: sound.sound_id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent guild_id", async () => { + await runEffect( + createGuildSoundboardSound({ + guild_id: NON_EXISTENT_GUILD_ID, + name: soundName("nf"), + sound: TEST_SOUND_DATA_URI ?? INVALID_SOUND_DATA_URI, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unseen guilds, but may surface as + // Forbidden (50001 Missing Access) when the bot can't see it, or + // BadRequest if Discord rejects the sound payload first. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - BadRequest for invalid (empty) sound data URI", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the BadRequest test", + ); + } + // An empty / malformed sound data URI is rejected with 400 Invalid Form + // Body. May also surface as Forbidden if the bot lacks + // CREATE_GUILD_EXPRESSIONS, or NotFound for an unseen guild. + await runEffect( + createGuildSoundboardSound({ + guild_id: TEST_GUILD_ID, + name: soundName("bad"), + sound: INVALID_SOUND_DATA_URI, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + createGuildSoundboardSound({ + guild_id: NON_EXISTENT_GUILD_ID, + name: soundName("fb"), + sound: TEST_SOUND_DATA_URI ?? INVALID_SOUND_DATA_URI, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createGuildSticker.test.ts b/packages/discord/test/createGuildSticker.test.ts new file mode 100644 index 000000000..732203566 --- /dev/null +++ b/packages/discord/test/createGuildSticker.test.ts @@ -0,0 +1,151 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildSticker } from "../src/operations/createGuildSticker.ts"; +import { deleteGuildSticker } from "../src/operations/deleteGuildSticker.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint requires: +// - a guild the bot is in with CREATE_GUILD_EXPRESSIONS permission. +// - a sticker file: PNG/APNG/Lottie at exactly 320x320 and <= 512KB. +// Operators must supply their own data URI via DISCORD_TEST_STICKER_DATA_URI; +// no inline fixture meets the size + dimension requirements safely. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_STICKER_DATA_URI = process.env.DISCORD_TEST_STICKER_DATA_URI; + +// Snowflake-format identifier that should not match a real guild. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; + +// Discord requires sticker names of 2..30 chars. +const stickerName = (suffix: string): string => + `dt-${suffix}-${testRunId}`.slice(0, 30); + +// A clearly invalid sticker payload — empty data URI — used for the BadRequest +// path; Discord rejects it with 400 Invalid Form Body. +const INVALID_STICKER_DATA_URI = "data:image/png;base64,"; + +describe("createGuildSticker", () => { + it("happy path - creates a guild sticker and deletes it on cleanup", async () => { + if (!TEST_GUILD_ID || !TEST_STICKER_DATA_URI) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_STICKER_DATA_URI env vars are required for the createGuildSticker happy path", + ); + } + const name = stickerName("happy"); + await runEffect( + Effect.gen(function* () { + const sticker = yield* createGuildSticker({ + guild_id: TEST_GUILD_ID, + name, + tags: "smile", + description: "distilled test sticker", + file: TEST_STICKER_DATA_URI, + }); + return yield* Effect.sync(() => { + expect(typeof sticker.id).toBe("string"); + expect(sticker.name).toBe(name); + expect(typeof sticker.tags).toBe("string"); + expect(typeof sticker.format_type).toBe("number"); + expect(typeof sticker.available).toBe("boolean"); + if (sticker.guild_id !== undefined) { + expect(sticker.guild_id).toBe(TEST_GUILD_ID); + } + }).pipe( + Effect.ensuring( + deleteGuildSticker({ + guild_id: TEST_GUILD_ID, + sticker_id: sticker.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent guild_id", async () => { + await runEffect( + createGuildSticker({ + guild_id: NON_EXISTENT_GUILD_ID, + name: stickerName("nf"), + tags: "smile", + file: TEST_STICKER_DATA_URI ?? INVALID_STICKER_DATA_URI, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unseen guilds, but may surface as + // Forbidden (50001 Missing Access) when the bot can't see it, or + // BadRequest if Discord rejects the sticker payload first. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - BadRequest for invalid (empty) sticker file data URI", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the BadRequest test", + ); + } + // An empty / malformed sticker file data URI is rejected with 400 Invalid + // Form Body. May also surface as Forbidden if the bot lacks + // CREATE_GUILD_EXPRESSIONS, or NotFound for an unseen guild. + await runEffect( + createGuildSticker({ + guild_id: TEST_GUILD_ID, + name: stickerName("bad"), + tags: "smile", + file: INVALID_STICKER_DATA_URI, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + createGuildSticker({ + guild_id: NON_EXISTENT_GUILD_ID, + name: stickerName("fb"), + tags: "smile", + file: TEST_STICKER_DATA_URI ?? INVALID_STICKER_DATA_URI, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createGuildTemplate.test.ts b/packages/discord/test/createGuildTemplate.test.ts new file mode 100644 index 000000000..d3cd2b602 --- /dev/null +++ b/packages/discord/test/createGuildTemplate.test.ts @@ -0,0 +1,122 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildTemplate } from "../src/operations/createGuildTemplate.ts"; +import { deleteGuildTemplate } from "../src/operations/deleteGuildTemplate.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a guild the bot is in with MANAGE_GUILD permission. Each guild can +// only have one template at a time, so the happy path immediately deletes the +// template it created. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifier that should not match a real guild. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; + +// Discord requires template names of 1..100 chars. +const templateName = (suffix: string): string => + `dt-${suffix}-${testRunId}`.slice(0, 100); + +describe("createGuildTemplate", () => { + it("happy path - creates a guild template and deletes it on cleanup", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the createGuildTemplate happy path", + ); + } + const name = templateName("happy"); + await runEffect( + Effect.gen(function* () { + const template = yield* createGuildTemplate({ + guild_id: TEST_GUILD_ID, + name, + description: "distilled test template", + }); + return yield* Effect.sync(() => { + expect(typeof template.code).toBe("string"); + expect(template.name).toBe(name); + expect(template.source_guild_id).toBe(TEST_GUILD_ID); + expect(typeof template.usage_count).toBe("number"); + expect(typeof template.creator_id).toBe("string"); + }).pipe( + Effect.ensuring( + deleteGuildTemplate({ + guild_id: TEST_GUILD_ID, + code: template.code, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent guild_id", async () => { + await runEffect( + createGuildTemplate({ + guild_id: NON_EXISTENT_GUILD_ID, + name: templateName("nf"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may surface 50001 Missing Access (Forbidden) instead of + // NotFound for guilds the bot can't see. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest when name is empty", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the BadRequest test", + ); + } + // Name must be 1..100 characters; empty string is rejected with 400 + // Invalid Form Body. + await runEffect( + createGuildTemplate({ + guild_id: TEST_GUILD_ID, + name: "", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + createGuildTemplate({ + guild_id: NON_EXISTENT_GUILD_ID, + name: templateName("fb"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createInteractionResponse.test.ts b/packages/discord/test/createInteractionResponse.test.ts new file mode 100644 index 000000000..b737e5470 --- /dev/null +++ b/packages/discord/test/createInteractionResponse.test.ts @@ -0,0 +1,124 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createInteractionResponse } from "../src/operations/createInteractionResponse.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint requires: +// - a fresh interaction_id + interaction_token captured from a real user +// interaction (e.g. a slash-command invocation). Tokens are valid for +// ~15 minutes and may only be acked once. +// The SDK's input schema currently exposes only the path parameters and the +// `with_response` query flag — the JSON callback body (`type`, `data`, etc.) +// is not exposed. As a result the operation performs a POST with no body, +// which Discord rejects with 400 Invalid Form Body. Operators must supply +// DISCORD_TEST_INTERACTION_ID + DISCORD_TEST_INTERACTION_TOKEN to attempt +// the happy path. +const TEST_INTERACTION_ID = process.env.DISCORD_TEST_INTERACTION_ID; +const TEST_INTERACTION_TOKEN = process.env.DISCORD_TEST_INTERACTION_TOKEN; + +// Snowflake-format identifier that should not match a real interaction. +const NON_EXISTENT_INTERACTION_ID = "100000000000000000"; +// Token shape is opaque; this is a clearly-bogus token used in error tests. +const NON_EXISTENT_INTERACTION_TOKEN = `notarealtoken-${testRunId}`; + +describe("createInteractionResponse", () => { + it("happy path - posts an interaction callback with with_response=true", async () => { + if (!TEST_INTERACTION_ID || !TEST_INTERACTION_TOKEN) { + throw new Error( + "DISCORD_TEST_INTERACTION_ID and DISCORD_TEST_INTERACTION_TOKEN env vars are required for the createInteractionResponse happy path", + ); + } + await runEffect( + createInteractionResponse({ + interaction_id: TEST_INTERACTION_ID, + interaction_token: TEST_INTERACTION_TOKEN, + with_response: true, + }).pipe( + Effect.tap((res) => + Effect.sync(() => { + expect(typeof res.interaction.id).toBe("string"); + expect(res.interaction.id).toBe(TEST_INTERACTION_ID); + }), + ), + ), + ); + }); + + it("error - NotFound for non-existent interaction_id/interaction_token", async () => { + await runEffect( + createInteractionResponse({ + interaction_id: NON_EXISTENT_INTERACTION_ID, + interaction_token: NON_EXISTENT_INTERACTION_TOKEN, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns 404 NotFound for unrecognized interaction tokens. + // It may also surface as 401 (covered as a generic case) or + // BadRequest because the empty callback body is rejected first. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "BadRequest", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - BadRequest when callback body is missing (codegen gap)", async () => { + // The SDK input does not expose the JSON callback body (`type`, `data`). + // POSTing without a body triggers 400 Invalid Form Body. Even with valid + // interaction credentials this should fail until the spec is patched. + await runEffect( + createInteractionResponse({ + interaction_id: TEST_INTERACTION_ID ?? NON_EXISTENT_INTERACTION_ID, + interaction_token: + TEST_INTERACTION_TOKEN ?? NON_EXISTENT_INTERACTION_TOKEN, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when the interaction token belongs to a different application", async () => { + // A snowflake-shaped interaction_id with a bogus token typically yields + // 404 NotFound, but Discord may classify access checks as 403 Forbidden + // when the token doesn't match the bot's application. + await runEffect( + createInteractionResponse({ + interaction_id: NON_EXISTENT_INTERACTION_ID, + interaction_token: NON_EXISTENT_INTERACTION_TOKEN, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createLinkedLobbyGuildInviteForSelf.test.ts b/packages/discord/test/createLinkedLobbyGuildInviteForSelf.test.ts new file mode 100644 index 000000000..eb50fb070 --- /dev/null +++ b/packages/discord/test/createLinkedLobbyGuildInviteForSelf.test.ts @@ -0,0 +1,109 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createLinkedLobbyGuildInviteForSelf } from "../src/operations/createLinkedLobbyGuildInviteForSelf.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint requires: +// - a lobby_id where the bot is a member. +// - that lobby must be linked to a guild (see linkChannelToLobby). +// Operators must supply DISCORD_TEST_LINKED_LOBBY_ID for the happy path. The +// resulting invite is a regular guild invite that auto-expires per Discord's +// default; no explicit cleanup is necessary, but we record the testRunId in +// surrounding logs for traceability. +const TEST_LINKED_LOBBY_ID = process.env.DISCORD_TEST_LINKED_LOBBY_ID; + +// Snowflake-format identifier that should not match a real lobby. +const NON_EXISTENT_LOBBY_ID = "100000000000000000"; + +describe("createLinkedLobbyGuildInviteForSelf", () => { + it("happy path - returns a guild invite code for the linked lobby", async () => { + if (!TEST_LINKED_LOBBY_ID) { + throw new Error( + "DISCORD_TEST_LINKED_LOBBY_ID env var is required for the createLinkedLobbyGuildInviteForSelf happy path (lobby must be linked to a guild and have the bot as a member)", + ); + } + // testRunId is included for traceability in any server-side audit logs. + void testRunId; + await runEffect( + createLinkedLobbyGuildInviteForSelf({ + lobby_id: TEST_LINKED_LOBBY_ID, + }).pipe( + Effect.tap((invite) => + Effect.sync(() => { + expect(typeof invite.code).toBe("string"); + expect(invite.code.length).toBeGreaterThan(0); + }), + ), + ), + ); + }); + + it("error - NotFound for non-existent lobby_id", async () => { + await runEffect( + createLinkedLobbyGuildInviteForSelf({ + lobby_id: NON_EXISTENT_LOBBY_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns 404 NotFound for unknown lobbies; may surface as + // 403 Forbidden if the bot lacks visibility into the route. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) lobby_id", async () => { + await runEffect( + createLinkedLobbyGuildInviteForSelf({ + lobby_id: "not-a-snowflake", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Malformed snowflake IDs are typically rejected with 400 Invalid + // Form Body, but the routing layer may also classify them as 404. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when the bot is not a member of the lobby", async () => { + // A snowflake-shaped lobby_id that the bot is not a member of typically + // yields 403 Forbidden, or 404 NotFound if the route 404s before the + // membership check. + await runEffect( + createLinkedLobbyGuildInviteForSelf({ + lobby_id: NON_EXISTENT_LOBBY_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createLinkedLobbyGuildInviteForUser.test.ts b/packages/discord/test/createLinkedLobbyGuildInviteForUser.test.ts new file mode 100644 index 000000000..5bf4a3c2c --- /dev/null +++ b/packages/discord/test/createLinkedLobbyGuildInviteForUser.test.ts @@ -0,0 +1,112 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createLinkedLobbyGuildInviteForUser } from "../src/operations/createLinkedLobbyGuildInviteForUser.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint requires: +// - a lobby_id linked to a guild. +// - a user_id that is a member of the lobby. +// Operators must supply DISCORD_TEST_LINKED_LOBBY_ID + DISCORD_TEST_LOBBY_USER_ID +// for the happy path. The resulting invite is a regular guild invite. +const TEST_LINKED_LOBBY_ID = process.env.DISCORD_TEST_LINKED_LOBBY_ID; +const TEST_LOBBY_USER_ID = process.env.DISCORD_TEST_LOBBY_USER_ID; + +// Snowflake-format identifiers that should not match a real lobby/user. +const NON_EXISTENT_LOBBY_ID = "100000000000000000"; +const NON_EXISTENT_USER_ID = "100000000000000001"; + +describe("createLinkedLobbyGuildInviteForUser", () => { + it("happy path - returns a guild invite code for the lobby member", async () => { + if (!TEST_LINKED_LOBBY_ID || !TEST_LOBBY_USER_ID) { + throw new Error( + "DISCORD_TEST_LINKED_LOBBY_ID and DISCORD_TEST_LOBBY_USER_ID env vars are required for the createLinkedLobbyGuildInviteForUser happy path", + ); + } + void testRunId; + await runEffect( + createLinkedLobbyGuildInviteForUser({ + lobby_id: TEST_LINKED_LOBBY_ID, + user_id: TEST_LOBBY_USER_ID, + }).pipe( + Effect.tap((invite) => + Effect.sync(() => { + expect(typeof invite.code).toBe("string"); + expect(invite.code.length).toBeGreaterThan(0); + }), + ), + ), + ); + }); + + it("error - NotFound for non-existent lobby_id", async () => { + await runEffect( + createLinkedLobbyGuildInviteForUser({ + lobby_id: NON_EXISTENT_LOBBY_ID, + user_id: TEST_LOBBY_USER_ID ?? NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns 404 NotFound for unknown lobbies; may also surface + // as 403 Forbidden if the bot lacks visibility. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) lobby_id", async () => { + await runEffect( + createLinkedLobbyGuildInviteForUser({ + lobby_id: "not-a-snowflake", + user_id: NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Malformed snowflakes are typically 400 Invalid Form Body, but the + // routing layer may also classify as 404. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when the user is not a member of the lobby", async () => { + // A snowflake-shaped user_id who is not a lobby member typically yields + // 403 Forbidden (or 404 NotFound if the route 404s before the membership + // check). + await runEffect( + createLinkedLobbyGuildInviteForUser({ + lobby_id: TEST_LINKED_LOBBY_ID ?? NON_EXISTENT_LOBBY_ID, + user_id: NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createLobby.test.ts b/packages/discord/test/createLobby.test.ts new file mode 100644 index 000000000..f6c70bfe1 --- /dev/null +++ b/packages/discord/test/createLobby.test.ts @@ -0,0 +1,113 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createLobby } from "../src/operations/createLobby.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// createLobby has no path parameters and no required body fields. The bot's +// token determines the application context. Lobbies are auto-cleaned by +// Discord after idle_timeout_seconds, so the happy path uses a small idle +// timeout for ephemerality. There is no deleteLobby operation in the SDK to +// run explicit cleanup against. + +// Snowflake-format identifier that should not match a real user. +const NON_EXISTENT_USER_ID = "100000000000000000"; + +describe("createLobby", () => { + it("happy path - creates an empty lobby with a short idle timeout", async () => { + void testRunId; + await runEffect( + createLobby({ + idle_timeout_seconds: 5, + metadata: { + distilled_test_run_id: testRunId, + }, + }).pipe( + Effect.tap((lobby) => + Effect.sync(() => { + expect(typeof lobby.id).toBe("string"); + expect(lobby.id.length).toBeGreaterThan(0); + expect(typeof lobby.application_id).toBe("string"); + expect(typeof lobby.flags).toBe("number"); + expect(Array.isArray(lobby.members)).toBe(true); + // Metadata round-trips on the response. + expect(lobby.metadata?.distilled_test_run_id).toBe(testRunId); + }), + ), + ), + ); + }); + + it("error - BadRequest for negative idle_timeout_seconds", async () => { + // Discord enforces a positive bound on idle_timeout_seconds; -1 is + // rejected with 400 Invalid Form Body. + await runEffect( + createLobby({ + idle_timeout_seconds: -1, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - NotFound for non-existent member user_id", async () => { + // Adding a member whose user_id does not resolve typically yields 404 + // NotFound (error code 10013 — user does not exist), but Discord may also classify this as + // 400 BadRequest (validation) or 403 Forbidden. + await runEffect( + createLobby({ + members: [ + { + id: NON_EXISTENT_USER_ID, + }, + ], + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "BadRequest", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden / BadRequest when override_event_webhooks_url is malformed", async () => { + // Discord requires override_event_webhooks_url to be a fully-qualified + // HTTPS URL. A non-URL string is rejected; some applications also lack + // the scope to set this field, in which case Discord returns 403. + await runEffect( + createLobby({ + override_event_webhooks_url: "not-a-valid-url", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "BadRequest", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createLobbyMessage.test.ts b/packages/discord/test/createLobbyMessage.test.ts new file mode 100644 index 000000000..b703f088e --- /dev/null +++ b/packages/discord/test/createLobbyMessage.test.ts @@ -0,0 +1,109 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createLobby } from "../src/operations/createLobby.ts"; +import { createLobbyMessage } from "../src/operations/createLobbyMessage.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Snowflake-format identifier that should not match a real lobby. +const NON_EXISTENT_LOBBY_ID = "100000000000000000"; + +describe("createLobbyMessage", () => { + it("happy path - posts a message to a freshly created lobby", async () => { + await runEffect( + Effect.gen(function* () { + const lobby = yield* createLobby({ idle_timeout_seconds: 5 }); + const content = `distilled-test-${testRunId}`; + const msg = yield* createLobbyMessage({ + lobby_id: lobby.id, + content, + }); + return yield* Effect.sync(() => { + expect(typeof msg.id).toBe("string"); + expect(msg.id.length).toBeGreaterThan(0); + expect(msg.content).toBe(content); + expect(msg.lobby_id).toBe(lobby.id); + expect(typeof msg.channel_id).toBe("string"); + expect(typeof msg.author.id).toBe("string"); + expect(typeof msg.flags).toBe("number"); + }); + }), + ); + }); + + it("error - NotFound for non-existent lobby_id", async () => { + await runEffect( + createLobbyMessage({ + lobby_id: NON_EXISTENT_LOBBY_ID, + content: `distilled-nf-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns 404 NotFound for unknown lobbies; may surface as + // 403 Forbidden if the bot lacks visibility. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest when content exceeds 2000 characters", async () => { + // Discord's per-message content limit is 2000 chars; 2001 chars triggers + // 400 Invalid Form Body. We create a real lobby first so the route + // resolves to the validation step rather than 404ing. + await runEffect( + Effect.gen(function* () { + const lobby = yield* createLobby({ idle_timeout_seconds: 5 }); + const tooLong = "a".repeat(2001); + return yield* createLobbyMessage({ + lobby_id: lobby.id, + content: tooLong, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ); + }), + ); + }); + + it("error - Forbidden when the bot is not a member of the lobby", async () => { + // Snowflake-shaped lobby_id the bot is not a member of typically yields + // 403 Forbidden, or 404 NotFound if the route 404s before the membership + // check. + await runEffect( + createLobbyMessage({ + lobby_id: NON_EXISTENT_LOBBY_ID, + content: `distilled-fb-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createMessage.test.ts b/packages/discord/test/createMessage.test.ts new file mode 100644 index 000000000..6ea87ddef --- /dev/null +++ b/packages/discord/test/createMessage.test.ts @@ -0,0 +1,124 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createMessage } from "../src/operations/createMessage.ts"; +import { deleteMessage } from "../src/operations/deleteMessage.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a text channel the bot can post to. Operators must supply +// DISCORD_TEST_CHANNEL_ID for the happy path. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifier that should not match a real channel. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; + +describe("createMessage", () => { + it("happy path - posts a message and deletes it on cleanup", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the createMessage happy path", + ); + } + const content = `distilled-test-${testRunId}`; + await runEffect( + Effect.gen(function* () { + const msg = yield* createMessage({ + channel_id: TEST_CHANNEL_ID, + content, + }); + return yield* Effect.sync(() => { + expect(typeof msg.id).toBe("string"); + expect(msg.id.length).toBeGreaterThan(0); + expect(msg.content).toBe(content); + expect(msg.channel_id).toBe(TEST_CHANNEL_ID); + expect(typeof msg.author.id).toBe("string"); + expect(typeof msg.timestamp).toBe("string"); + expect(typeof msg.flags).toBe("number"); + }).pipe( + Effect.ensuring( + deleteMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent channel_id", async () => { + await runEffect( + createMessage({ + channel_id: NON_EXISTENT_CHANNEL_ID, + content: `distilled-nf-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns 404 NotFound for unknown channels; may surface as + // 403 Forbidden if the bot lacks visibility. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for empty payload (no content / embeds / attachments / stickers)", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the BadRequest test", + ); + } + // Discord requires at least one of content, embeds, sticker_ids, + // components, or attachments. Posting an empty body returns 400 Invalid + // Form Body. + await runEffect( + createMessage({ + channel_id: TEST_CHANNEL_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when posting to a channel the bot cannot see", async () => { + // A snowflake-shaped channel_id the bot cannot see typically yields 403 + // Forbidden (50001 Missing Access), or 404 NotFound if the route 404s + // before the permission check. + await runEffect( + createMessage({ + channel_id: NON_EXISTENT_CHANNEL_ID, + content: `distilled-fb-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createOrJoinLobby.test.ts b/packages/discord/test/createOrJoinLobby.test.ts new file mode 100644 index 000000000..91fe96471 --- /dev/null +++ b/packages/discord/test/createOrJoinLobby.test.ts @@ -0,0 +1,112 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createOrJoinLobby } from "../src/operations/createOrJoinLobby.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// createOrJoinLobby has no path parameters. The `secret` is a stable +// identifier per application — calling with the same secret joins the +// existing lobby, calling with a fresh secret creates a new one. Lobbies +// auto-clean via idle_timeout_seconds; there is no deleteLobby operation. + +describe("createOrJoinLobby", () => { + it("happy path - creates a lobby for a fresh secret and idempotently joins it", async () => { + const secret = `distilled-secret-${testRunId}`; + await runEffect( + Effect.gen(function* () { + const first = yield* createOrJoinLobby({ + secret, + idle_timeout_seconds: 5, + lobby_metadata: { + distilled_test_run_id: testRunId, + }, + }); + const second = yield* createOrJoinLobby({ + secret, + idle_timeout_seconds: 5, + }); + return yield* Effect.sync(() => { + expect(typeof first.id).toBe("string"); + expect(first.id.length).toBeGreaterThan(0); + expect(typeof first.application_id).toBe("string"); + expect(typeof first.flags).toBe("number"); + expect(first.metadata?.distilled_test_run_id).toBe(testRunId); + // Same secret should resolve to the same lobby. + expect(second.id).toBe(first.id); + }); + }), + ); + }); + + it("error - BadRequest for negative idle_timeout_seconds", async () => { + // Discord enforces a positive bound on idle_timeout_seconds; -1 is + // rejected with 400 Invalid Form Body. + await runEffect( + createOrJoinLobby({ + secret: `distilled-bad-${testRunId}`, + idle_timeout_seconds: -1, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - NotFound / BadRequest when override fields reference non-resolvable values", async () => { + // Some optional fields require resolvable references (e.g. metadata + // entries that name resources). Discord may return 404 NotFound or 400 + // BadRequest for unrecognized values. + await runEffect( + createOrJoinLobby({ + secret: `distilled-nf-${testRunId}`, + // Empty string is not a valid metadata key; Discord rejects it. + lobby_metadata: { "": "v" }, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "BadRequest", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden / BadRequest when secret is empty", async () => { + // The secret is required for the create-or-join semantics. An empty + // secret is rejected; some applications also lack the scope to call + // this endpoint, in which case Discord returns 403. + await runEffect( + createOrJoinLobby({ + secret: "", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "BadRequest", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createPin.test.ts b/packages/discord/test/createPin.test.ts new file mode 100644 index 000000000..d2834e4c9 --- /dev/null +++ b/packages/discord/test/createPin.test.ts @@ -0,0 +1,137 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createMessage } from "../src/operations/createMessage.ts"; +import { createPin } from "../src/operations/createPin.ts"; +import { deleteMessage } from "../src/operations/deleteMessage.ts"; +import { deletePin } from "../src/operations/deletePin.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a text channel where the bot can post and pin messages. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; +const NON_EXISTENT_MESSAGE_ID = "100000000000000001"; + +describe("createPin", () => { + it("happy path - pins a freshly created message and unpins on cleanup", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the createPin happy path", + ); + } + await runEffect( + Effect.gen(function* () { + const msg = yield* createMessage({ + channel_id: TEST_CHANNEL_ID, + content: `distilled-pin-${testRunId}`, + }); + return yield* createPin({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe( + Effect.tap(() => + Effect.sync(() => { + // 204 No Content; output schema is Void. + expect(true).toBe(true); + }), + ), + Effect.ensuring( + deletePin({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe(Effect.ignore), + ), + Effect.ensuring( + deleteMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent message_id in a real channel", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the NotFound test", + ); + } + // Discord returns 404 NotFound (10008 — message does not exist) when the + // message_id does not exist in the channel. + await runEffect( + createPin({ + channel_id: TEST_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) message_id", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the BadRequest test", + ); + } + // Malformed snowflakes are typically rejected with 400 Invalid Form Body, + // but the routing layer may also classify as 404. + await runEffect( + createPin({ + channel_id: TEST_CHANNEL_ID, + message_id: "not-a-snowflake", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when the channel cannot be seen by the bot", async () => { + // A snowflake-shaped channel_id the bot cannot see typically yields 403 + // Forbidden (50001 Missing Access), or 404 NotFound if the route 404s + // before the permission check. + await runEffect( + createPin({ + channel_id: NON_EXISTENT_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createStageInstance.test.ts b/packages/discord/test/createStageInstance.test.ts new file mode 100644 index 000000000..c5b8900e1 --- /dev/null +++ b/packages/discord/test/createStageInstance.test.ts @@ -0,0 +1,129 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createStageInstance } from "../src/operations/createStageInstance.ts"; +import { deleteStageInstance } from "../src/operations/deleteStageInstance.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a stage channel (channel type 13) where the bot has +// MANAGE_CHANNELS / MUTE_MEMBERS / MOVE_MEMBERS. The bot must also be a +// stage moderator. Operators must supply DISCORD_TEST_STAGE_CHANNEL_ID for +// the happy path. +const TEST_STAGE_CHANNEL_ID = process.env.DISCORD_TEST_STAGE_CHANNEL_ID; + +// Snowflake-format identifier that should not match a real channel. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; + +// Discord requires a topic of 1..120 chars. +const stageTopic = (suffix: string): string => + `dt-${suffix}-${testRunId}`.slice(0, 120); + +describe("createStageInstance", () => { + it("happy path - creates a stage instance and deletes it on cleanup", async () => { + if (!TEST_STAGE_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_STAGE_CHANNEL_ID env var is required for the createStageInstance happy path (channel must be a stage channel)", + ); + } + const topic = stageTopic("happy"); + await runEffect( + Effect.gen(function* () { + const stage = yield* createStageInstance({ + channel_id: TEST_STAGE_CHANNEL_ID, + topic, + }); + return yield* Effect.sync(() => { + expect(typeof stage.id).toBe("string"); + expect(stage.id.length).toBeGreaterThan(0); + expect(stage.channel_id).toBe(TEST_STAGE_CHANNEL_ID); + expect(stage.topic).toBe(topic); + expect(typeof stage.guild_id).toBe("string"); + expect(typeof stage.discoverable_disabled).toBe("boolean"); + }).pipe( + Effect.ensuring( + deleteStageInstance({ + channel_id: TEST_STAGE_CHANNEL_ID, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent channel_id", async () => { + await runEffect( + createStageInstance({ + channel_id: NON_EXISTENT_CHANNEL_ID, + topic: stageTopic("nf"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns 404 NotFound for unknown channels; may surface as + // 403 Forbidden if the bot lacks visibility. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest when topic is empty", async () => { + if (!TEST_STAGE_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_STAGE_CHANNEL_ID env var is required for the BadRequest test", + ); + } + // Topic must be 1..120 characters; empty string is rejected with 400 + // Invalid Form Body. + await runEffect( + createStageInstance({ + channel_id: TEST_STAGE_CHANNEL_ID, + topic: "", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a channel the bot cannot moderate", async () => { + // A snowflake-shaped channel_id the bot does not see typically yields 403 + // Forbidden (50001 Missing Access), or 404 NotFound if the route 404s + // before the permission check. A non-stage channel returns 400 BadRequest + // (50079 — channel must be a stage channel). + await runEffect( + createStageInstance({ + channel_id: NON_EXISTENT_CHANNEL_ID, + topic: stageTopic("fb"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createThread.test.ts b/packages/discord/test/createThread.test.ts new file mode 100644 index 000000000..f742d8309 --- /dev/null +++ b/packages/discord/test/createThread.test.ts @@ -0,0 +1,130 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createThread } from "../src/operations/createThread.ts"; +import { deleteChannel } from "../src/operations/deleteChannel.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint requires: +// - a text/forum/media channel where the bot can create threads. +// The SDK's input schema currently only exposes the path parameter +// (channel_id) and not the JSON body (name, type, auto_archive_duration, +// invitable, rate_limit_per_user). Discord rejects an empty body with 400 +// Invalid Form Body, so the happy path is documented as a codegen gap and +// is gated on DISCORD_TEST_ALLOW_EMPTY_THREAD_BODY=1 alongside +// DISCORD_TEST_CHANNEL_ID. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; +const ALLOW_EMPTY_THREAD_BODY = + process.env.DISCORD_TEST_ALLOW_EMPTY_THREAD_BODY === "1"; + +// Snowflake-format identifier that should not match a real channel. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; + +describe("createThread", () => { + it("happy path - calls createThread against a real channel and asserts the response", async () => { + if (!TEST_CHANNEL_ID || !ALLOW_EMPTY_THREAD_BODY) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID and DISCORD_TEST_ALLOW_EMPTY_THREAD_BODY=1 are required for the createThread happy path. The SDK input does not yet expose the JSON body (name, type, auto_archive_duration, ...); set the flag to opt in to the empty-body call so the codegen gap is observable.", + ); + } + void testRunId; + await runEffect( + Effect.gen(function* () { + const thread = yield* createThread({ + channel_id: TEST_CHANNEL_ID, + }); + return yield* Effect.sync(() => { + expect(typeof thread.id).toBe("string"); + expect(thread.id.length).toBeGreaterThan(0); + expect(typeof thread.name).toBe("string"); + expect(typeof thread.guild_id).toBe("string"); + expect(typeof thread.owner_id).toBe("string"); + expect(typeof thread.flags).toBe("number"); + expect(typeof thread.thread_metadata.archived).toBe("boolean"); + }).pipe( + Effect.ensuring( + deleteChannel({ channel_id: thread.id }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent channel_id", async () => { + await runEffect( + createThread({ + channel_id: NON_EXISTENT_CHANNEL_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns 404 NotFound for unknown channels; may surface as + // 403 Forbidden if the bot lacks visibility, or BadRequest if the + // empty body is rejected first. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - BadRequest when thread body is missing (codegen gap)", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the BadRequest test", + ); + } + // The SDK input does not expose the JSON body. POSTing without a `name` + // field triggers 400 Invalid Form Body. Even with a real channel this + // should fail until the spec is patched to expose the body. + await runEffect( + createThread({ + channel_id: TEST_CHANNEL_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a channel the bot cannot see", async () => { + // A snowflake-shaped channel_id the bot does not see typically yields 403 + // Forbidden (50001 Missing Access), or 404 NotFound if the route 404s + // before the permission check. + await runEffect( + createThread({ + channel_id: NON_EXISTENT_CHANNEL_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createThreadFromMessage.test.ts b/packages/discord/test/createThreadFromMessage.test.ts new file mode 100644 index 000000000..00533c439 --- /dev/null +++ b/packages/discord/test/createThreadFromMessage.test.ts @@ -0,0 +1,147 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createMessage } from "../src/operations/createMessage.ts"; +import { createThreadFromMessage } from "../src/operations/createThreadFromMessage.ts"; +import { deleteChannel } from "../src/operations/deleteChannel.ts"; +import { deleteMessage } from "../src/operations/deleteMessage.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a text channel where the bot can post messages and create threads. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; +const NON_EXISTENT_MESSAGE_ID = "100000000000000001"; + +// Discord requires thread names of 1..100 chars. +const threadName = (suffix: string): string => + `dt-${suffix}-${testRunId}`.slice(0, 100); + +describe("createThreadFromMessage", () => { + it("happy path - creates a thread from a freshly posted message and deletes both on cleanup", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the createThreadFromMessage happy path", + ); + } + const name = threadName("happy"); + await runEffect( + Effect.gen(function* () { + const msg = yield* createMessage({ + channel_id: TEST_CHANNEL_ID, + content: `distilled-thread-from-msg-${testRunId}`, + }); + const thread = yield* createThreadFromMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + name, + }).pipe( + Effect.ensuring( + // The thread's id is the message id by Discord convention; deleting + // the thread channel cleans up. We also delete the source message. + deleteMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe(Effect.ignore), + ), + ); + return yield* Effect.sync(() => { + expect(typeof thread.id).toBe("string"); + expect(thread.name).toBe(name); + expect(typeof thread.guild_id).toBe("string"); + expect(typeof thread.owner_id).toBe("string"); + expect(typeof thread.flags).toBe("number"); + expect(typeof thread.thread_metadata.archived).toBe("boolean"); + }).pipe( + Effect.ensuring( + deleteChannel({ channel_id: thread.id }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent message_id in a real channel", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the NotFound test", + ); + } + await runEffect( + createThreadFromMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + name: threadName("nf"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest when name is empty", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the BadRequest test", + ); + } + // Thread name must be 1..100 characters; empty string is rejected with + // 400 Invalid Form Body. We use a real channel so the request reaches + // the validation step; the bogus message_id may also surface as 404. + await runEffect( + createThreadFromMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + name: "", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a channel the bot cannot see", async () => { + // A snowflake-shaped channel_id the bot cannot see typically yields 403 + // Forbidden (50001 Missing Access), or 404 NotFound if the route 404s + // before the permission check. + await runEffect( + createThreadFromMessage({ + channel_id: NON_EXISTENT_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + name: threadName("fb"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/createWebhook.test.ts b/packages/discord/test/createWebhook.test.ts new file mode 100644 index 000000000..c240f1d38 --- /dev/null +++ b/packages/discord/test/createWebhook.test.ts @@ -0,0 +1,123 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createWebhook } from "../src/operations/createWebhook.ts"; +import { deleteWebhook } from "../src/operations/deleteWebhook.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a text/announcement/forum channel where the bot has +// MANAGE_WEBHOOKS. Operators must supply DISCORD_TEST_CHANNEL_ID for the +// happy path. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifier that should not match a real channel. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; + +// Discord requires webhook names of 1..80 chars and disallows certain +// substrings ("clyde", "discord"). +const webhookName = (suffix: string): string => + `dt-${suffix}-${testRunId}`.slice(0, 80); + +describe("createWebhook", () => { + it("happy path - creates a webhook and deletes it on cleanup", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the createWebhook happy path", + ); + } + const name = webhookName("happy"); + await runEffect( + Effect.gen(function* () { + const webhook = yield* createWebhook({ + channel_id: TEST_CHANNEL_ID, + name, + }); + return yield* Effect.sync(() => { + expect(typeof webhook.id).toBe("string"); + expect(webhook.id.length).toBeGreaterThan(0); + expect(webhook.name).toBe(name); + }).pipe( + Effect.ensuring( + deleteWebhook({ webhook_id: webhook.id }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent channel_id", async () => { + await runEffect( + createWebhook({ + channel_id: NON_EXISTENT_CHANNEL_ID, + name: webhookName("nf"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns 404 NotFound for unknown channels; may surface as + // 403 Forbidden if the bot lacks visibility. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest when name is empty", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the BadRequest test", + ); + } + // Webhook name must be 1..80 chars; empty string is rejected with 400 + // Invalid Form Body. + await runEffect( + createWebhook({ + channel_id: TEST_CHANNEL_ID, + name: "", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a channel the bot cannot manage", async () => { + // A snowflake-shaped channel_id the bot cannot see typically yields 403 + // Forbidden (50001 Missing Access), or 404 NotFound if the route 404s + // before the permission check. + await runEffect( + createWebhook({ + channel_id: NON_EXISTENT_CHANNEL_ID, + name: webhookName("fb"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/crosspostMessage.test.ts b/packages/discord/test/crosspostMessage.test.ts new file mode 100644 index 000000000..0a791828a --- /dev/null +++ b/packages/discord/test/crosspostMessage.test.ts @@ -0,0 +1,154 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createMessage } from "../src/operations/createMessage.ts"; +import { crosspostMessage } from "../src/operations/crosspostMessage.ts"; +import { deleteMessage } from "../src/operations/deleteMessage.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Crossposting requires the channel to be an announcement channel (type 5) +// where the bot has SEND_MESSAGES + MANAGE_MESSAGES (own message can be +// crossposted with just SEND_MESSAGES). Operators must supply +// DISCORD_TEST_ANNOUNCEMENT_CHANNEL_ID for the happy path. +const TEST_ANNOUNCEMENT_CHANNEL_ID = + process.env.DISCORD_TEST_ANNOUNCEMENT_CHANNEL_ID; +// A regular text channel for the BadRequest test (50019 — A message can only +// be crossposted in an announcement channel). +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; +const NON_EXISTENT_MESSAGE_ID = "100000000000000001"; + +describe("crosspostMessage", () => { + it("happy path - crossposts a freshly created announcement message", async () => { + if (!TEST_ANNOUNCEMENT_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_ANNOUNCEMENT_CHANNEL_ID env var is required for the crosspostMessage happy path (channel must be an announcement / news channel)", + ); + } + const content = `distilled-crosspost-${testRunId}`; + await runEffect( + Effect.gen(function* () { + const msg = yield* createMessage({ + channel_id: TEST_ANNOUNCEMENT_CHANNEL_ID, + content, + }); + return yield* crosspostMessage({ + channel_id: TEST_ANNOUNCEMENT_CHANNEL_ID, + message_id: msg.id, + }).pipe( + Effect.tap((res) => + Effect.sync(() => { + expect(res.id).toBe(msg.id); + expect(res.channel_id).toBe(TEST_ANNOUNCEMENT_CHANNEL_ID); + expect(res.content).toBe(content); + expect(typeof res.flags).toBe("number"); + // The CROSSPOSTED message flag (bit 0 / value 1) is set on + // successful crosspost. + expect((res.flags & 1) === 1).toBe(true); + }), + ), + Effect.ensuring( + deleteMessage({ + channel_id: TEST_ANNOUNCEMENT_CHANNEL_ID, + message_id: msg.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent message_id in a real channel", async () => { + if (!TEST_ANNOUNCEMENT_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_ANNOUNCEMENT_CHANNEL_ID env var is required for the NotFound test", + ); + } + await runEffect( + crosspostMessage({ + channel_id: TEST_ANNOUNCEMENT_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest when crossposting from a non-announcement channel", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the BadRequest test (regular text channel)", + ); + } + // Discord error 50019 — a message can only be crossposted in an + // announcement channel. Posting to a regular text channel and attempting + // to crosspost yields 400 Invalid Form Body. + await runEffect( + Effect.gen(function* () { + const msg = yield* createMessage({ + channel_id: TEST_CHANNEL_ID, + content: `distilled-crosspost-bad-${testRunId}`, + }); + return yield* crosspostMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + Effect.ensuring( + deleteMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - Forbidden when targeting a channel the bot cannot see", async () => { + // A snowflake-shaped channel_id the bot cannot see typically yields 403 + // Forbidden (50001 Missing Access), or 404 NotFound if the route 404s + // before the permission check. + await runEffect( + crosspostMessage({ + channel_id: NON_EXISTENT_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteAllMessageReactions.test.ts b/packages/discord/test/deleteAllMessageReactions.test.ts new file mode 100644 index 000000000..9840b7226 --- /dev/null +++ b/packages/discord/test/deleteAllMessageReactions.test.ts @@ -0,0 +1,104 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createMessage } from "../src/operations/createMessage.ts"; +import { deleteAllMessageReactions } from "../src/operations/deleteAllMessageReactions.ts"; +import { deleteMessage } from "../src/operations/deleteMessage.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a text channel where the bot can post and manage messages. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; +const NON_EXISTENT_MESSAGE_ID = "100000000000000001"; + +describe("deleteAllMessageReactions", () => { + it("happy path - clears reactions on a freshly posted message", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the deleteAllMessageReactions happy path", + ); + } + void testRunId; + await runEffect( + Effect.gen(function* () { + const msg = yield* createMessage({ + channel_id: TEST_CHANNEL_ID, + content: `distilled-clear-reactions-${testRunId}`, + }); + return yield* deleteAllMessageReactions({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe( + Effect.tap(() => + // 204 No Content; output schema is Void. Calling against a + // message with zero reactions is a valid no-op. + Effect.sync(() => { + expect(true).toBe(true); + }), + ), + Effect.ensuring( + deleteMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent message_id in a real channel", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the NotFound test", + ); + } + // Discord returns 404 NotFound (10008 — message does not exist) when the + // message_id does not exist in the channel. + await runEffect( + deleteAllMessageReactions({ + channel_id: TEST_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when targeting a channel the bot cannot see", async () => { + // A snowflake-shaped channel_id the bot cannot see typically yields 403 + // Forbidden (50001 Missing Access), or 404 NotFound if the route 404s + // before the permission check. + await runEffect( + deleteAllMessageReactions({ + channel_id: NON_EXISTENT_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteAllMessageReactionsByEmoji.test.ts b/packages/discord/test/deleteAllMessageReactionsByEmoji.test.ts new file mode 100644 index 000000000..04635bd2a --- /dev/null +++ b/packages/discord/test/deleteAllMessageReactionsByEmoji.test.ts @@ -0,0 +1,111 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createMessage } from "../src/operations/createMessage.ts"; +import { deleteAllMessageReactionsByEmoji } from "../src/operations/deleteAllMessageReactionsByEmoji.ts"; +import { deleteMessage } from "../src/operations/deleteMessage.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a text channel where the bot can post and manage messages. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; +const NON_EXISTENT_MESSAGE_ID = "100000000000000001"; + +// A standard unicode emoji used as the {emoji_name} path segment. Discord +// accepts the raw unicode character or `name:id` for custom emoji. +const EMOJI = "👍"; + +describe("deleteAllMessageReactionsByEmoji", () => { + it("happy path - clears reactions for a specific emoji on a freshly posted message", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the deleteAllMessageReactionsByEmoji happy path", + ); + } + void testRunId; + await runEffect( + Effect.gen(function* () { + const msg = yield* createMessage({ + channel_id: TEST_CHANNEL_ID, + content: `distilled-clear-emoji-${testRunId}`, + }); + return yield* deleteAllMessageReactionsByEmoji({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + emoji_name: EMOJI, + }).pipe( + Effect.tap(() => + // 204 No Content; output schema is Void. Calling against a + // message with zero reactions for this emoji is a valid no-op. + Effect.sync(() => { + expect(true).toBe(true); + }), + ), + Effect.ensuring( + deleteMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent message_id in a real channel", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the NotFound test", + ); + } + // Discord returns 404 NotFound (10008 — message does not exist) when the + // message_id does not exist in the channel. + await runEffect( + deleteAllMessageReactionsByEmoji({ + channel_id: TEST_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + emoji_name: EMOJI, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when targeting a channel the bot cannot see", async () => { + // A snowflake-shaped channel_id the bot cannot see typically yields 403 + // Forbidden (50001 Missing Access), or 404 NotFound if the route 404s + // before the permission check. + await runEffect( + deleteAllMessageReactionsByEmoji({ + channel_id: NON_EXISTENT_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + emoji_name: EMOJI, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteApplicationCommand.test.ts b/packages/discord/test/deleteApplicationCommand.test.ts new file mode 100644 index 000000000..9f8f8e572 --- /dev/null +++ b/packages/discord/test/deleteApplicationCommand.test.ts @@ -0,0 +1,104 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createApplicationCommand } from "../src/operations/createApplicationCommand.ts"; +import { deleteApplicationCommand } from "../src/operations/deleteApplicationCommand.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires the bot's application_id. Operators must supply +// DISCORD_TEST_APPLICATION_ID for the happy path so a real command can be +// created and then deleted. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; +const NON_EXISTENT_COMMAND_ID = "100000000000000001"; + +// Discord requires command names to match `^[-_\p{L}\p{N}\p{sc=Devanagari}\p{sc=Thai}]{1,32}$` +// in lowercase. +const commandName = (suffix: string): string => + `dt_${suffix}_${testRunId}`.toLowerCase().slice(0, 32); + +describe("deleteApplicationCommand", () => { + it("happy path - deletes a freshly created application command", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the deleteApplicationCommand happy path", + ); + } + const name = commandName("del"); + await runEffect( + Effect.gen(function* () { + const command = yield* createApplicationCommand({ + application_id: TEST_APPLICATION_ID, + name, + description: "distilled test command (will be deleted)", + }); + return yield* deleteApplicationCommand({ + application_id: TEST_APPLICATION_ID, + command_id: command.id, + }).pipe( + Effect.tap(() => + // 204 No Content; output schema is Void. + Effect.sync(() => { + expect(true).toBe(true); + }), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent command_id", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the NotFound test", + ); + } + // Discord returns 404 NotFound for command_ids that do not exist on the + // application. + await runEffect( + deleteApplicationCommand({ + application_id: TEST_APPLICATION_ID, + command_id: NON_EXISTENT_COMMAND_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when the bot does not own the application_id", async () => { + // A snowflake-shaped application_id the bot's token does not own + // typically yields 403 Forbidden, or 404 NotFound if the route 404s + // before the ownership check. + await runEffect( + deleteApplicationCommand({ + application_id: NON_EXISTENT_APPLICATION_ID, + command_id: NON_EXISTENT_COMMAND_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteApplicationEmoji.test.ts b/packages/discord/test/deleteApplicationEmoji.test.ts new file mode 100644 index 000000000..1791306f1 --- /dev/null +++ b/packages/discord/test/deleteApplicationEmoji.test.ts @@ -0,0 +1,106 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createApplicationEmoji } from "../src/operations/createApplicationEmoji.ts"; +import { deleteApplicationEmoji } from "../src/operations/deleteApplicationEmoji.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Smallest valid 1x1 transparent PNG, encoded as a data URI. +const TINY_PNG_DATA_URI = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII="; + +// Requires the bot's application_id. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; +const NON_EXISTENT_EMOJI_ID = "100000000000000001"; + +// Discord requires emoji names to match ^[a-zA-Z0-9_]{2,32}$. +const emojiName = (suffix: string): string => { + const raw = `dt_${suffix}_${testRunId}`; + return raw.replace(/[^a-zA-Z0-9_]/g, "_").slice(0, 32); +}; + +describe("deleteApplicationEmoji", () => { + it("happy path - deletes a freshly created application emoji", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the deleteApplicationEmoji happy path", + ); + } + await runEffect( + Effect.gen(function* () { + const emoji = yield* createApplicationEmoji({ + application_id: TEST_APPLICATION_ID, + name: emojiName("del"), + image: TINY_PNG_DATA_URI, + }); + return yield* deleteApplicationEmoji({ + application_id: TEST_APPLICATION_ID, + emoji_id: emoji.id, + }).pipe( + Effect.tap(() => + // 204 No Content; output schema is Void. + Effect.sync(() => { + expect(true).toBe(true); + }), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent emoji_id", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the NotFound test", + ); + } + // Discord returns 404 NotFound for emoji_ids that do not exist on the + // application. + await runEffect( + deleteApplicationEmoji({ + application_id: TEST_APPLICATION_ID, + emoji_id: NON_EXISTENT_EMOJI_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when the bot does not own the application_id", async () => { + // A snowflake-shaped application_id the bot's token does not own + // typically yields 403 Forbidden, or 404 NotFound if the route 404s + // before the ownership check. + await runEffect( + deleteApplicationEmoji({ + application_id: NON_EXISTENT_APPLICATION_ID, + emoji_id: NON_EXISTENT_EMOJI_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteApplicationUserRoleConnection.test.ts b/packages/discord/test/deleteApplicationUserRoleConnection.test.ts new file mode 100644 index 000000000..b66842905 --- /dev/null +++ b/packages/discord/test/deleteApplicationUserRoleConnection.test.ts @@ -0,0 +1,87 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { deleteApplicationUserRoleConnection } from "../src/operations/deleteApplicationUserRoleConnection.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint is /users/@me/applications/{application_id}/role-connection +// and requires a user OAuth2 bearer token with the `role_connections.write` +// scope — bot tokens cannot use it. This is a destructive operation that +// removes the calling user's role connection on the application. Operators +// who have a user token configured must opt in with +// DISCORD_TEST_APPLICATION_ID + DISCORD_TEST_ALLOW_DELETE_USER_ROLE_CONNECTION=1. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; +const ALLOW_DELETE_USER_ROLE_CONNECTION = + process.env.DISCORD_TEST_ALLOW_DELETE_USER_ROLE_CONNECTION === "1"; + +// Snowflake-format identifier that should not match a real application. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; + +describe("deleteApplicationUserRoleConnection", () => { + it("happy path - deletes the calling user's role connection on the application", async () => { + if (!TEST_APPLICATION_ID || !ALLOW_DELETE_USER_ROLE_CONNECTION) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID and DISCORD_TEST_ALLOW_DELETE_USER_ROLE_CONNECTION=1 are required for the deleteApplicationUserRoleConnection happy path. The endpoint requires a user OAuth2 bearer token with role_connections.write scope; this DELETE removes the user's role connection.", + ); + } + void testRunId; + await runEffect( + deleteApplicationUserRoleConnection({ + application_id: TEST_APPLICATION_ID, + }).pipe( + Effect.tap(() => + // 204 No Content; output schema is Void. + Effect.sync(() => { + expect(true).toBe(true); + }), + ), + ), + ); + }); + + it("error - NotFound for non-existent application_id", async () => { + // Discord returns 404 NotFound for application_ids that do not exist or + // have no role connection for the calling user. + await runEffect( + deleteApplicationUserRoleConnection({ + application_id: NON_EXISTENT_APPLICATION_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when the token lacks role_connections.write scope", async () => { + // Bot tokens cannot use this endpoint — Discord returns 403 Forbidden + // (or 401 in some configurations). User OAuth2 tokens missing the + // role_connections.write scope return 403. May also surface as 404. + await runEffect( + deleteApplicationUserRoleConnection({ + application_id: NON_EXISTENT_APPLICATION_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteAutoModerationRule.test.ts b/packages/discord/test/deleteAutoModerationRule.test.ts new file mode 100644 index 000000000..e2594e70a --- /dev/null +++ b/packages/discord/test/deleteAutoModerationRule.test.ts @@ -0,0 +1,95 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { deleteAutoModerationRule } from "../src/operations/deleteAutoModerationRule.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint deletes an auto-moderation rule by id. The corresponding +// createAutoModerationRule operation has a codegen gap — its input only +// exposes guild_id and not the rule body — so we cannot reliably create a +// rule through the SDK to delete. Operators must supply a pre-existing +// throwaway rule via DISCORD_TEST_GUILD_ID + DISCORD_TEST_AUTO_MODERATION_RULE_ID +// for the happy path (this DELETE removes it). +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_AUTO_MODERATION_RULE_ID = + process.env.DISCORD_TEST_AUTO_MODERATION_RULE_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_RULE_ID = "100000000000000001"; + +describe("deleteAutoModerationRule", () => { + it("happy path - deletes a pre-existing auto-moderation rule", async () => { + if (!TEST_GUILD_ID || !TEST_AUTO_MODERATION_RULE_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_AUTO_MODERATION_RULE_ID env vars are required for the deleteAutoModerationRule happy path. createAutoModerationRule has a codegen gap (no body), so the rule must be created out-of-band.", + ); + } + void testRunId; + await runEffect( + deleteAutoModerationRule({ + guild_id: TEST_GUILD_ID, + rule_id: TEST_AUTO_MODERATION_RULE_ID, + }).pipe( + Effect.tap(() => + // 204 No Content; output schema is Void. + Effect.sync(() => { + expect(true).toBe(true); + }), + ), + ), + ); + }); + + it("error - NotFound for non-existent rule_id in a real guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + // Discord returns 404 NotFound for rule_ids that do not exist on the guild. + await runEffect( + deleteAutoModerationRule({ + guild_id: TEST_GUILD_ID, + rule_id: NON_EXISTENT_RULE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot cannot moderate", async () => { + // A snowflake-shaped guild_id the bot cannot see typically yields 403 + // Forbidden (50001 Missing Access), or 404 NotFound if the route 404s + // before the permission check. + await runEffect( + deleteAutoModerationRule({ + guild_id: NON_EXISTENT_GUILD_ID, + rule_id: NON_EXISTENT_RULE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteChannel.test.ts b/packages/discord/test/deleteChannel.test.ts new file mode 100644 index 000000000..4bd088dbc --- /dev/null +++ b/packages/discord/test/deleteChannel.test.ts @@ -0,0 +1,101 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildChannel } from "../src/operations/createGuildChannel.ts"; +import { deleteChannel } from "../src/operations/deleteChannel.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a guild where the bot has Manage Channels. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifier that should not match a real channel. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; + +const channelName = (suffix: string): string => + `dt-delch-${suffix}-${testRunId}`.slice(0, 100); + +describe("deleteChannel", () => { + it( + "happy path - creates a channel then deletes it and returns the deleted channel", + async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the deleteChannel happy path", + ); + } + await runEffect( + Effect.gen(function* () { + const created = yield* createGuildChannel({ + guild_id: TEST_GUILD_ID, + name: channelName("happy"), + // type 0 = GUILD_TEXT + type: 0, + }); + const result = yield* deleteChannel({ + channel_id: created.id, + }).pipe( + // If deleteChannel fails for any reason, ensure we still try to + // clean up the channel we just created. + Effect.ensuring( + deleteChannel({ channel_id: created.id }).pipe(Effect.ignore), + ), + ); + return yield* Effect.sync(() => { + // Discord returns the deleted channel object. The SDK types it as + // unknown, so narrow defensively before asserting. + expect(result).toBeDefined(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const r = result as any; + expect(typeof r.id).toBe("string"); + expect(r.id).toBe(created.id); + }); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for non-existent channel_id", async () => { + await runEffect( + deleteChannel({ channel_id: NON_EXISTENT_CHANNEL_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // A snowflake-shaped channel_id the bot cannot see typically + // surfaces as 404 NotFound. Discord may also return 403 Forbidden + // (50001 Missing Access) if the route reaches the permission check. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden / NotFound for a malformed channel_id", async () => { + // A non-snowflake string is rejected by Discord's routing layer. + await runEffect( + deleteChannel({ channel_id: "not-a-snowflake" }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteChannelPermissionOverwrite.test.ts b/packages/discord/test/deleteChannelPermissionOverwrite.test.ts new file mode 100644 index 000000000..010200112 --- /dev/null +++ b/packages/discord/test/deleteChannelPermissionOverwrite.test.ts @@ -0,0 +1,151 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildChannel } from "../src/operations/createGuildChannel.ts"; +import { deleteChannel } from "../src/operations/deleteChannel.ts"; +import { deleteChannelPermissionOverwrite } from "../src/operations/deleteChannelPermissionOverwrite.ts"; +import { setChannelPermissionOverwrite } from "../src/operations/setChannelPermissionOverwrite.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a guild where the bot has Manage Channels + Manage Roles. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; +const NON_EXISTENT_OVERWRITE_ID = "100000000000000001"; + +const channelName = (suffix: string): string => + `dt-delperm-${suffix}-${testRunId}`.slice(0, 100); + +describe("deleteChannelPermissionOverwrite", () => { + it( + "happy path - sets a role overwrite on a fresh channel and deletes it", + async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the deleteChannelPermissionOverwrite happy path", + ); + } + // The @everyone role id equals the guild id by Discord convention, + // making it a stable target for a permission overwrite (type 0 = role). + const roleId = TEST_GUILD_ID; + await runEffect( + Effect.gen(function* () { + const channel = yield* createGuildChannel({ + guild_id: TEST_GUILD_ID, + name: channelName("happy"), + type: 0, + }); + return yield* Effect.gen(function* () { + yield* setChannelPermissionOverwrite({ + channel_id: channel.id, + overwrite_id: roleId, + type: 0, + // VIEW_CHANNEL bit, deny it for the role. + allow: 0, + deny: 1024, + }); + const result = yield* deleteChannelPermissionOverwrite({ + channel_id: channel.id, + overwrite_id: roleId, + }); + return yield* Effect.sync(() => { + expect(result).toBeUndefined(); + }); + }).pipe( + Effect.ensuring( + deleteChannel({ channel_id: channel.id }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for non-existent channel_id", async () => { + await runEffect( + deleteChannelPermissionOverwrite({ + channel_id: NON_EXISTENT_CHANNEL_ID, + overwrite_id: NON_EXISTENT_OVERWRITE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // A snowflake-shaped channel_id the bot cannot see typically yields + // 404 NotFound. Discord may also surface 403 Forbidden (50001 + // Missing Access) if the route reaches the permission check. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it( + "error - NotFound for an overwrite_id that does not exist on a real channel", + async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + // Create a fresh channel that has no overwrites, then try to delete a + // bogus overwrite_id on it. Discord returns 404 (error code 10009) or + // 403 depending on which check fires first. + await runEffect( + Effect.gen(function* () { + const channel = yield* createGuildChannel({ + guild_id: TEST_GUILD_ID, + name: channelName("nf"), + type: 0, + }); + return yield* deleteChannelPermissionOverwrite({ + channel_id: channel.id, + overwrite_id: NON_EXISTENT_OVERWRITE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + Effect.ensuring( + deleteChannel({ channel_id: channel.id }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - Forbidden for a malformed channel_id", async () => { + await runEffect( + deleteChannelPermissionOverwrite({ + channel_id: "not-a-snowflake", + overwrite_id: NON_EXISTENT_OVERWRITE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteEntitlement.test.ts b/packages/discord/test/deleteEntitlement.test.ts new file mode 100644 index 000000000..c9f87b026 --- /dev/null +++ b/packages/discord/test/deleteEntitlement.test.ts @@ -0,0 +1,117 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createEntitlement } from "../src/operations/createEntitlement.ts"; +import { deleteEntitlement } from "../src/operations/deleteEntitlement.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The application's snowflake id (the bot's own application). +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; +// A SKU id from the application's monetization settings; required to create +// a test entitlement that we can then delete. +const TEST_SKU_ID = process.env.DISCORD_TEST_SKU_ID; +// A guild that we own and can use as the entitlement owner. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifier that should not match a real entity. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; +const NON_EXISTENT_ENTITLEMENT_ID = "100000000000000001"; + +describe("deleteEntitlement", () => { + it( + "happy path - creates a test entitlement then deletes it", + async () => { + if (!TEST_APPLICATION_ID || !TEST_SKU_ID || !TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID, DISCORD_TEST_SKU_ID and DISCORD_TEST_GUILD_ID env vars are required for the deleteEntitlement happy path. " + + `(testRunId=${testRunId})`, + ); + } + await runEffect( + Effect.gen(function* () { + const created = yield* createEntitlement({ + application_id: TEST_APPLICATION_ID, + sku_id: TEST_SKU_ID, + owner_id: TEST_GUILD_ID, + // owner_type 1 = GUILD_SUBSCRIPTION + owner_type: 1, + }); + const result = yield* deleteEntitlement({ + application_id: TEST_APPLICATION_ID, + entitlement_id: created.id, + }).pipe( + // If deleteEntitlement fails, still try to clean up the test + // entitlement we just created. + Effect.ensuring( + deleteEntitlement({ + application_id: TEST_APPLICATION_ID, + entitlement_id: created.id, + }).pipe(Effect.ignore), + ), + ); + return yield* Effect.sync(() => { + expect(result).toBeUndefined(); + }); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for non-existent entitlement_id", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the NotFound test", + ); + } + await runEffect( + deleteEntitlement({ + application_id: TEST_APPLICATION_ID, + entitlement_id: NON_EXISTENT_ENTITLEMENT_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // A snowflake-shaped entitlement_id that does not exist on the + // application yields 404 NotFound. Discord may also surface 403 + // Forbidden depending on which check fires first. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it( + "error - Forbidden for an application_id that is not the bot's", + async () => { + // The bot can only manage entitlements for its own application. + // Targeting another application_id results in 403 Forbidden, or 404 + // NotFound if the route 404s before the permission check. + await runEffect( + deleteEntitlement({ + application_id: NON_EXISTENT_APPLICATION_ID, + entitlement_id: NON_EXISTENT_ENTITLEMENT_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }, + ); +}); diff --git a/packages/discord/test/deleteGroupDmUser.test.ts b/packages/discord/test/deleteGroupDmUser.test.ts new file mode 100644 index 000000000..600dde9b3 --- /dev/null +++ b/packages/discord/test/deleteGroupDmUser.test.ts @@ -0,0 +1,98 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { deleteGroupDmUser } from "../src/operations/deleteGroupDmUser.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Group DM management requires a user OAuth2 token with the `gdm.join` scope +// and the caller must be the group DM owner. The happy path is also opt-in +// because it actually removes a real user from a real group DM. +const TEST_GROUP_DM_CHANNEL_ID = process.env.DISCORD_TEST_GROUP_DM_CHANNEL_ID; +const TEST_GROUP_DM_USER_ID = process.env.DISCORD_TEST_GROUP_DM_USER_ID; +const ALLOW_REMOVE_GROUP_DM_USER = + process.env.DISCORD_TEST_ALLOW_REMOVE_GROUP_DM_USER === "1"; + +// Snowflake-format identifiers that should not match a real entity. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; +const NON_EXISTENT_USER_ID = "100000000000000001"; + +describe("deleteGroupDmUser", () => { + it( + "happy path - removes a recipient from a group DM", + async () => { + if ( + !TEST_GROUP_DM_CHANNEL_ID || + !TEST_GROUP_DM_USER_ID || + !ALLOW_REMOVE_GROUP_DM_USER + ) { + throw new Error( + "DISCORD_TEST_GROUP_DM_CHANNEL_ID, DISCORD_TEST_GROUP_DM_USER_ID and DISCORD_TEST_ALLOW_REMOVE_GROUP_DM_USER=1 are required for the deleteGroupDmUser happy path. " + + `(testRunId=${testRunId})`, + ); + } + const result = await runEffect( + deleteGroupDmUser({ + channel_id: TEST_GROUP_DM_CHANNEL_ID, + user_id: TEST_GROUP_DM_USER_ID, + }), + ); + expect(result).toBeUndefined(); + }, + 30_000, + ); + + it("error - NotFound for non-existent channel_id", async () => { + await runEffect( + deleteGroupDmUser({ + channel_id: NON_EXISTENT_CHANNEL_ID, + user_id: NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // A snowflake-shaped channel_id that the caller cannot see yields + // 404 NotFound, or 403 Forbidden if the route reaches the + // permission check first. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it( + "error - Forbidden when the bot is not the group DM owner", + async () => { + // Bot tokens cannot manage group DMs at all — even if the channel_id + // and user_id are well-formed, Discord returns 403 Forbidden (or 404 + // NotFound when the route 404s before the permission check). + await runEffect( + deleteGroupDmUser({ + channel_id: NON_EXISTENT_CHANNEL_ID, + user_id: "not-a-snowflake", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }, + ); +}); diff --git a/packages/discord/test/deleteGuildApplicationCommand.test.ts b/packages/discord/test/deleteGuildApplicationCommand.test.ts new file mode 100644 index 000000000..195b1d8a5 --- /dev/null +++ b/packages/discord/test/deleteGuildApplicationCommand.test.ts @@ -0,0 +1,121 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildApplicationCommand } from "../src/operations/createGuildApplicationCommand.ts"; +import { deleteGuildApplicationCommand } from "../src/operations/deleteGuildApplicationCommand.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The bot's own application snowflake. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; +// A guild where the bot is installed with the application.commands scope. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; +const NON_EXISTENT_GUILD_ID = "100000000000000001"; +const NON_EXISTENT_COMMAND_ID = "100000000000000002"; + +// Slash command names must be 1..32 chars and lowercase. +const commandName = (suffix: string): string => + `dt_delgcmd_${suffix}_${testRunId}`.slice(0, 32); + +describe("deleteGuildApplicationCommand", () => { + it( + "happy path - creates a guild command then deletes it", + async () => { + if (!TEST_APPLICATION_ID || !TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID and DISCORD_TEST_GUILD_ID env vars are required for the deleteGuildApplicationCommand happy path", + ); + } + await runEffect( + Effect.gen(function* () { + const created = yield* createGuildApplicationCommand({ + application_id: TEST_APPLICATION_ID, + guild_id: TEST_GUILD_ID, + name: commandName("happy"), + description: `distilled test ${testRunId}`, + }); + const result = yield* deleteGuildApplicationCommand({ + application_id: TEST_APPLICATION_ID, + guild_id: TEST_GUILD_ID, + command_id: created.id, + }).pipe( + // If the delete fails, still try to clean up the command we + // just created. + Effect.ensuring( + deleteGuildApplicationCommand({ + application_id: TEST_APPLICATION_ID, + guild_id: TEST_GUILD_ID, + command_id: created.id, + }).pipe(Effect.ignore), + ), + ); + return yield* Effect.sync(() => { + expect(result).toBeUndefined(); + }); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for non-existent command_id", async () => { + if (!TEST_APPLICATION_ID || !TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID and DISCORD_TEST_GUILD_ID env vars are required for the NotFound test", + ); + } + await runEffect( + deleteGuildApplicationCommand({ + application_id: TEST_APPLICATION_ID, + guild_id: TEST_GUILD_ID, + command_id: NON_EXISTENT_COMMAND_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // A snowflake-shaped command_id that does not exist on the + // application/guild yields 404 NotFound. Discord may also surface + // 403 Forbidden depending on which check fires first. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it( + "error - Forbidden for an application_id that is not the bot's", + async () => { + // The bot can only manage commands for its own application. Targeting + // another application_id results in 403 Forbidden, or 404 NotFound if + // the route 404s before the permission check. + await runEffect( + deleteGuildApplicationCommand({ + application_id: NON_EXISTENT_APPLICATION_ID, + guild_id: NON_EXISTENT_GUILD_ID, + command_id: NON_EXISTENT_COMMAND_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }, + ); +}); diff --git a/packages/discord/test/deleteGuildEmoji.test.ts b/packages/discord/test/deleteGuildEmoji.test.ts new file mode 100644 index 000000000..9c61d3682 --- /dev/null +++ b/packages/discord/test/deleteGuildEmoji.test.ts @@ -0,0 +1,117 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildEmoji } from "../src/operations/createGuildEmoji.ts"; +import { deleteGuildEmoji } from "../src/operations/deleteGuildEmoji.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Smallest valid 1x1 transparent PNG, encoded as a data URI. +const TINY_PNG_DATA_URI = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII="; + +// Requires a guild where the bot has Manage Emojis and Stickers. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_EMOJI_ID = "100000000000000001"; + +// Discord requires emoji names to match ^[a-zA-Z0-9_]{2,32}$. +const emojiName = (suffix: string): string => { + const raw = `dt_${suffix}_${testRunId}`; + return raw.replace(/[^a-zA-Z0-9_]/g, "_").slice(0, 32); +}; + +describe("deleteGuildEmoji", () => { + it( + "happy path - deletes a freshly created guild emoji", + async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the deleteGuildEmoji happy path", + ); + } + await runEffect( + Effect.gen(function* () { + const emoji = yield* createGuildEmoji({ + guild_id: TEST_GUILD_ID, + name: emojiName("del"), + image: TINY_PNG_DATA_URI, + }); + const result = yield* deleteGuildEmoji({ + guild_id: TEST_GUILD_ID, + emoji_id: emoji.id, + }).pipe( + // If the delete fails, still attempt to remove the emoji we + // just created. + Effect.ensuring( + deleteGuildEmoji({ + guild_id: TEST_GUILD_ID, + emoji_id: emoji.id, + }).pipe(Effect.ignore), + ), + ); + return yield* Effect.sync(() => { + // 204 No Content; output schema is Void. + expect(result).toBeUndefined(); + }); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for non-existent emoji_id", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + // A snowflake-shaped emoji_id that does not exist on the guild yields + // 404 NotFound. Discord may also surface 403 Forbidden depending on + // which check fires first. + await runEffect( + deleteGuildEmoji({ + guild_id: TEST_GUILD_ID, + emoji_id: NON_EXISTENT_EMOJI_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for a guild_id the bot is not in", async () => { + // A guild_id the bot is not a member of typically yields 403 Forbidden + // (50001 Missing Access), or 404 NotFound if the route 404s before the + // permission check. + await runEffect( + deleteGuildEmoji({ + guild_id: NON_EXISTENT_GUILD_ID, + emoji_id: NON_EXISTENT_EMOJI_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteGuildIntegration.test.ts b/packages/discord/test/deleteGuildIntegration.test.ts new file mode 100644 index 000000000..bb401bc1d --- /dev/null +++ b/packages/discord/test/deleteGuildIntegration.test.ts @@ -0,0 +1,100 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { deleteGuildIntegration } from "../src/operations/deleteGuildIntegration.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Guild integrations cannot be created via the bot REST API — they are +// installed when a user authorizes an OAuth2 application, connects a +// Twitch/YouTube/X account, or installs a bot. The happy path therefore +// requires a pre-existing throwaway integration plus an explicit opt-in +// flag because the deletion is destructive and not reversible from the API. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_INTEGRATION_ID = process.env.DISCORD_TEST_INTEGRATION_ID; +const ALLOW_DELETE_INTEGRATION = + process.env.DISCORD_TEST_ALLOW_DELETE_INTEGRATION === "1"; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_INTEGRATION_ID = "100000000000000001"; + +describe("deleteGuildIntegration", () => { + it( + "happy path - removes a pre-existing throwaway integration from the guild", + async () => { + if ( + !TEST_GUILD_ID || + !TEST_INTEGRATION_ID || + !ALLOW_DELETE_INTEGRATION + ) { + throw new Error( + "DISCORD_TEST_GUILD_ID, DISCORD_TEST_INTEGRATION_ID and DISCORD_TEST_ALLOW_DELETE_INTEGRATION=1 are required for the deleteGuildIntegration happy path. " + + `(testRunId=${testRunId})`, + ); + } + const result = await runEffect( + deleteGuildIntegration({ + guild_id: TEST_GUILD_ID, + integration_id: TEST_INTEGRATION_ID, + }), + ); + // 204 No Content; output schema is Void. + expect(result).toBeUndefined(); + }, + 30_000, + ); + + it("error - NotFound for non-existent integration_id on a real guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + // A snowflake-shaped integration_id that does not exist on the guild + // yields 404 NotFound. Discord may also surface 403 Forbidden depending + // on which check fires first. + await runEffect( + deleteGuildIntegration({ + guild_id: TEST_GUILD_ID, + integration_id: NON_EXISTENT_INTEGRATION_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for a guild_id the bot is not in", async () => { + // A guild_id the bot is not a member of typically yields 403 Forbidden + // (50001 Missing Access), or 404 NotFound if the route 404s before the + // permission check. + await runEffect( + deleteGuildIntegration({ + guild_id: NON_EXISTENT_GUILD_ID, + integration_id: NON_EXISTENT_INTEGRATION_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteGuildMember.test.ts b/packages/discord/test/deleteGuildMember.test.ts new file mode 100644 index 000000000..221405825 --- /dev/null +++ b/packages/discord/test/deleteGuildMember.test.ts @@ -0,0 +1,96 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { deleteGuildMember } from "../src/operations/deleteGuildMember.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// deleteGuildMember kicks a real user out of a real guild. New members can +// only be added via OAuth2 access tokens (PUT /guilds/{guild_id}/members +// requires the joined user's access_token, which we cannot mint here), so +// the happy path requires a pre-existing throwaway member plus an opt-in +// flag. The kicked user can rejoin via invite afterwards. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_KICK_USER_ID = process.env.DISCORD_TEST_KICK_USER_ID; +const ALLOW_KICK_MEMBER = + process.env.DISCORD_TEST_ALLOW_KICK_MEMBER === "1"; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_USER_ID = "100000000000000001"; + +describe("deleteGuildMember", () => { + it( + "happy path - kicks a pre-existing throwaway member from the guild", + async () => { + if (!TEST_GUILD_ID || !TEST_KICK_USER_ID || !ALLOW_KICK_MEMBER) { + throw new Error( + "DISCORD_TEST_GUILD_ID, DISCORD_TEST_KICK_USER_ID and DISCORD_TEST_ALLOW_KICK_MEMBER=1 are required for the deleteGuildMember happy path. " + + `(testRunId=${testRunId})`, + ); + } + const result = await runEffect( + deleteGuildMember({ + guild_id: TEST_GUILD_ID, + user_id: TEST_KICK_USER_ID, + }), + ); + // 204 No Content; output schema is Void. + expect(result).toBeUndefined(); + }, + 30_000, + ); + + it("error - NotFound for a user_id that is not a member of the guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + // A snowflake-shaped user_id that is not a member of the guild yields + // 404 NotFound (10007). Discord may also surface 403 Forbidden depending + // on which check fires first. + await runEffect( + deleteGuildMember({ + guild_id: TEST_GUILD_ID, + user_id: NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for a guild_id the bot is not in", async () => { + // A guild_id the bot is not a member of typically yields 403 Forbidden + // (50001 Missing Access), or 404 NotFound if the route 404s before the + // permission check. + await runEffect( + deleteGuildMember({ + guild_id: NON_EXISTENT_GUILD_ID, + user_id: NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteGuildMemberRole.test.ts b/packages/discord/test/deleteGuildMemberRole.test.ts new file mode 100644 index 000000000..07cb5ff90 --- /dev/null +++ b/packages/discord/test/deleteGuildMemberRole.test.ts @@ -0,0 +1,125 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { addGuildMemberRole } from "../src/operations/addGuildMemberRole.ts"; +import { deleteGuildMemberRole } from "../src/operations/deleteGuildMemberRole.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires: +// - a guild where the bot has Manage Roles +// - a user_id that is currently a member of that guild +// - a role_id in that guild whose position is BELOW the bot's highest role +// Roles cannot be created in isolation here without polluting the guild on +// every run, so the role is provided via env. The test grants then removes +// the role on the member; the member's other roles are untouched. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_MEMBER_USER_ID = process.env.DISCORD_TEST_MEMBER_USER_ID; +const TEST_ROLE_ID = process.env.DISCORD_TEST_ROLE_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_USER_ID = "100000000000000001"; +const NON_EXISTENT_ROLE_ID = "100000000000000002"; + +describe("deleteGuildMemberRole", () => { + it( + "happy path - grants then removes a role on a real guild member", + async () => { + if (!TEST_GUILD_ID || !TEST_MEMBER_USER_ID || !TEST_ROLE_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID, DISCORD_TEST_MEMBER_USER_ID and DISCORD_TEST_ROLE_ID env vars are required for the deleteGuildMemberRole happy path. " + + `(testRunId=${testRunId})`, + ); + } + await runEffect( + Effect.gen(function* () { + // Grant the role first so removing it is observable. Both + // operations are idempotent on Discord's side. + yield* addGuildMemberRole({ + guild_id: TEST_GUILD_ID, + user_id: TEST_MEMBER_USER_ID, + role_id: TEST_ROLE_ID, + }); + const result = yield* deleteGuildMemberRole({ + guild_id: TEST_GUILD_ID, + user_id: TEST_MEMBER_USER_ID, + role_id: TEST_ROLE_ID, + }).pipe( + // If the delete fails, attempt the cleanup explicitly. + Effect.ensuring( + deleteGuildMemberRole({ + guild_id: TEST_GUILD_ID, + user_id: TEST_MEMBER_USER_ID, + role_id: TEST_ROLE_ID, + }).pipe(Effect.ignore), + ), + ); + return yield* Effect.sync(() => { + // 204 No Content; output schema is Void. + expect(result).toBeUndefined(); + }); + }), + ); + }, + 30_000, + ); + + it( + "error - NotFound for a role_id that does not exist in the guild", + async () => { + if (!TEST_GUILD_ID || !TEST_MEMBER_USER_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_MEMBER_USER_ID env vars are required for the NotFound test", + ); + } + // A snowflake-shaped role_id that is not a role of the guild yields + // 404 NotFound. Discord may also surface 403 Forbidden depending on + // which check fires first. + await runEffect( + deleteGuildMemberRole({ + guild_id: TEST_GUILD_ID, + user_id: TEST_MEMBER_USER_ID, + role_id: NON_EXISTENT_ROLE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }, + ); + + it("error - Forbidden for a guild_id the bot is not in", async () => { + // A guild_id the bot is not a member of typically yields 403 Forbidden + // (50001 Missing Access), or 404 NotFound if the route 404s before the + // permission check. + await runEffect( + deleteGuildMemberRole({ + guild_id: NON_EXISTENT_GUILD_ID, + user_id: NON_EXISTENT_USER_ID, + role_id: NON_EXISTENT_ROLE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteGuildRole.test.ts b/packages/discord/test/deleteGuildRole.test.ts new file mode 100644 index 000000000..479941205 --- /dev/null +++ b/packages/discord/test/deleteGuildRole.test.ts @@ -0,0 +1,113 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildRole } from "../src/operations/createGuildRole.ts"; +import { deleteGuildRole } from "../src/operations/deleteGuildRole.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a guild where the bot has Manage Roles. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_ROLE_ID = "100000000000000001"; + +// Role names must be 1..100 chars. +const roleName = (suffix: string): string => + `dt-delrole-${suffix}-${testRunId}`.slice(0, 100); + +describe("deleteGuildRole", () => { + it( + "happy path - creates a role then deletes it", + async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the deleteGuildRole happy path", + ); + } + await runEffect( + Effect.gen(function* () { + const role = yield* createGuildRole({ + guild_id: TEST_GUILD_ID, + name: roleName("happy"), + // No permissions, no hoist, not mentionable. + permissions: 0, + hoist: false, + mentionable: false, + }); + const result = yield* deleteGuildRole({ + guild_id: TEST_GUILD_ID, + role_id: role.id, + }).pipe( + // If the delete fails, attempt cleanup explicitly. + Effect.ensuring( + deleteGuildRole({ + guild_id: TEST_GUILD_ID, + role_id: role.id, + }).pipe(Effect.ignore), + ), + ); + return yield* Effect.sync(() => { + // 204 No Content; output schema is Void. + expect(result).toBeUndefined(); + }); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for non-existent role_id", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + // A snowflake-shaped role_id that is not a role of the guild yields + // 404 NotFound. Discord may also surface 403 Forbidden depending on + // which check fires first. + await runEffect( + deleteGuildRole({ + guild_id: TEST_GUILD_ID, + role_id: NON_EXISTENT_ROLE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for a guild_id the bot is not in", async () => { + // A guild_id the bot is not a member of typically yields 403 Forbidden + // (50001 Missing Access), or 404 NotFound if the route 404s before the + // permission check. + await runEffect( + deleteGuildRole({ + guild_id: NON_EXISTENT_GUILD_ID, + role_id: NON_EXISTENT_ROLE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteGuildScheduledEvent.test.ts b/packages/discord/test/deleteGuildScheduledEvent.test.ts new file mode 100644 index 000000000..1d92b09aa --- /dev/null +++ b/packages/discord/test/deleteGuildScheduledEvent.test.ts @@ -0,0 +1,94 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { deleteGuildScheduledEvent } from "../src/operations/deleteGuildScheduledEvent.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// createGuildScheduledEvent has a codegen gap — none of the required body +// fields (name, scheduled_start_time, entity_type, privacy_level, ...) are +// exposed on the SDK input. The happy path therefore requires a pre-existing +// throwaway scheduled event whose id is provided via env. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_GUILD_SCHEDULED_EVENT_ID = + process.env.DISCORD_TEST_GUILD_SCHEDULED_EVENT_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_GUILD_SCHEDULED_EVENT_ID = "100000000000000001"; + +describe("deleteGuildScheduledEvent", () => { + it( + "happy path - deletes a pre-existing throwaway scheduled event", + async () => { + if (!TEST_GUILD_ID || !TEST_GUILD_SCHEDULED_EVENT_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_GUILD_SCHEDULED_EVENT_ID env vars are required for the deleteGuildScheduledEvent happy path. " + + `(testRunId=${testRunId})`, + ); + } + const result = await runEffect( + deleteGuildScheduledEvent({ + guild_id: TEST_GUILD_ID, + guild_scheduled_event_id: TEST_GUILD_SCHEDULED_EVENT_ID, + }), + ); + // 204 No Content; output schema is Void. + expect(result).toBeUndefined(); + }, + 30_000, + ); + + it("error - NotFound for non-existent guild_scheduled_event_id", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + // A snowflake-shaped guild_scheduled_event_id that does not exist on the + // guild yields 404 NotFound. Discord may also surface 403 Forbidden + // depending on which check fires first. + await runEffect( + deleteGuildScheduledEvent({ + guild_id: TEST_GUILD_ID, + guild_scheduled_event_id: NON_EXISTENT_GUILD_SCHEDULED_EVENT_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for a guild_id the bot is not in", async () => { + // A guild_id the bot is not a member of typically yields 403 Forbidden + // (50001 Missing Access), or 404 NotFound if the route 404s before the + // permission check. + await runEffect( + deleteGuildScheduledEvent({ + guild_id: NON_EXISTENT_GUILD_ID, + guild_scheduled_event_id: NON_EXISTENT_GUILD_SCHEDULED_EVENT_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteGuildSoundboardSound.test.ts b/packages/discord/test/deleteGuildSoundboardSound.test.ts new file mode 100644 index 000000000..f807bc2df --- /dev/null +++ b/packages/discord/test/deleteGuildSoundboardSound.test.ts @@ -0,0 +1,115 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildSoundboardSound } from "../src/operations/createGuildSoundboardSound.ts"; +import { deleteGuildSoundboardSound } from "../src/operations/deleteGuildSoundboardSound.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a guild where the bot has Manage Guild Expressions and a small +// MP3/OGG sound clip (<=512KB, <=5.2s) provided as a data URI. Discord does +// not accept synthetic placeholder audio, so the clip must come from env. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_SOUNDBOARD_SOUND_DATA_URI = + process.env.DISCORD_TEST_SOUNDBOARD_SOUND_DATA_URI; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_SOUND_ID = "100000000000000001"; + +// Soundboard sound names must be 2..32 chars. +const soundName = (suffix: string): string => + `dt-sb-${suffix}-${testRunId}`.slice(0, 32); + +describe("deleteGuildSoundboardSound", () => { + it( + "happy path - creates a soundboard sound then deletes it", + async () => { + if (!TEST_GUILD_ID || !TEST_SOUNDBOARD_SOUND_DATA_URI) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_SOUNDBOARD_SOUND_DATA_URI env vars are required for the deleteGuildSoundboardSound happy path. " + + `(testRunId=${testRunId})`, + ); + } + await runEffect( + Effect.gen(function* () { + const sound = yield* createGuildSoundboardSound({ + guild_id: TEST_GUILD_ID, + name: soundName("del"), + sound: TEST_SOUNDBOARD_SOUND_DATA_URI, + }); + const result = yield* deleteGuildSoundboardSound({ + guild_id: TEST_GUILD_ID, + sound_id: sound.sound_id, + }).pipe( + // If the delete fails, attempt cleanup explicitly. + Effect.ensuring( + deleteGuildSoundboardSound({ + guild_id: TEST_GUILD_ID, + sound_id: sound.sound_id, + }).pipe(Effect.ignore), + ), + ); + return yield* Effect.sync(() => { + // 204 No Content; output schema is Void. + expect(result).toBeUndefined(); + }); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for non-existent sound_id", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + // A snowflake-shaped sound_id that does not exist on the guild yields + // 404 NotFound. Discord may also surface 403 Forbidden depending on + // which check fires first. + await runEffect( + deleteGuildSoundboardSound({ + guild_id: TEST_GUILD_ID, + sound_id: NON_EXISTENT_SOUND_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for a guild_id the bot is not in", async () => { + // A guild_id the bot is not a member of typically yields 403 Forbidden + // (50001 Missing Access), or 404 NotFound if the route 404s before the + // permission check. + await runEffect( + deleteGuildSoundboardSound({ + guild_id: NON_EXISTENT_GUILD_ID, + sound_id: NON_EXISTENT_SOUND_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteGuildSticker.test.ts b/packages/discord/test/deleteGuildSticker.test.ts new file mode 100644 index 000000000..3913c984f --- /dev/null +++ b/packages/discord/test/deleteGuildSticker.test.ts @@ -0,0 +1,118 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildSticker } from "../src/operations/createGuildSticker.ts"; +import { deleteGuildSticker } from "../src/operations/deleteGuildSticker.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires: +// - a guild the bot is in with MANAGE_GUILD_EXPRESSIONS permission. +// - a sticker file (PNG/APNG/Lottie at exactly 320x320, <=512KB) provided +// as a data URI via env. No inline fixture meets the size + dimension +// requirements, so the operator must supply one. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_STICKER_DATA_URI = process.env.DISCORD_TEST_STICKER_DATA_URI; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_STICKER_ID = "100000000000000001"; + +// Discord requires sticker names of 2..30 chars. +const stickerName = (suffix: string): string => + `dt-${suffix}-${testRunId}`.slice(0, 30); + +describe("deleteGuildSticker", () => { + it( + "happy path - creates a guild sticker then deletes it", + async () => { + if (!TEST_GUILD_ID || !TEST_STICKER_DATA_URI) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_STICKER_DATA_URI env vars are required for the deleteGuildSticker happy path. " + + `(testRunId=${testRunId})`, + ); + } + await runEffect( + Effect.gen(function* () { + const sticker = yield* createGuildSticker({ + guild_id: TEST_GUILD_ID, + name: stickerName("del"), + tags: "smile", + description: "distilled test sticker", + file: TEST_STICKER_DATA_URI, + }); + const result = yield* deleteGuildSticker({ + guild_id: TEST_GUILD_ID, + sticker_id: sticker.id, + }).pipe( + // If the delete fails, attempt cleanup explicitly. + Effect.ensuring( + deleteGuildSticker({ + guild_id: TEST_GUILD_ID, + sticker_id: sticker.id, + }).pipe(Effect.ignore), + ), + ); + return yield* Effect.sync(() => { + // 204 No Content; output schema is Void. + expect(result).toBeUndefined(); + }); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for non-existent sticker_id", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + // A snowflake-shaped sticker_id that does not exist on the guild yields + // 404 NotFound. Discord may also surface 403 Forbidden depending on + // which check fires first. + await runEffect( + deleteGuildSticker({ + guild_id: TEST_GUILD_ID, + sticker_id: NON_EXISTENT_STICKER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for a guild_id the bot is not in", async () => { + // A guild_id the bot is not a member of typically yields 403 Forbidden + // (50001 Missing Access), or 404 NotFound if the route 404s before the + // permission check. + await runEffect( + deleteGuildSticker({ + guild_id: NON_EXISTENT_GUILD_ID, + sticker_id: NON_EXISTENT_STICKER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteGuildTemplate.test.ts b/packages/discord/test/deleteGuildTemplate.test.ts new file mode 100644 index 000000000..7562aeea4 --- /dev/null +++ b/packages/discord/test/deleteGuildTemplate.test.ts @@ -0,0 +1,120 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildTemplate } from "../src/operations/createGuildTemplate.ts"; +import { deleteGuildTemplate } from "../src/operations/deleteGuildTemplate.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a guild where the bot has Manage Guild. A guild may have at most +// one template at a time, so we delete first if necessary, create one fresh, +// then delete it. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifier that should not match a real guild. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +// A template code that should not exist on any guild. +const NON_EXISTENT_TEMPLATE_CODE = `dt-no-such-${testRunId}`; + +// Discord requires template names of 1..100 chars. +const templateName = (suffix: string): string => + `dt-${suffix}-${testRunId}`.slice(0, 100); + +describe("deleteGuildTemplate", () => { + it( + "happy path - creates a guild template then deletes it and returns it", + async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the deleteGuildTemplate happy path", + ); + } + await runEffect( + Effect.gen(function* () { + const created = yield* createGuildTemplate({ + guild_id: TEST_GUILD_ID, + name: templateName("del"), + description: "distilled test template", + }); + const result = yield* deleteGuildTemplate({ + guild_id: TEST_GUILD_ID, + code: created.code, + }).pipe( + // If the delete fails, attempt cleanup explicitly. + Effect.ensuring( + deleteGuildTemplate({ + guild_id: TEST_GUILD_ID, + code: created.code, + }).pipe(Effect.ignore), + ), + ); + return yield* Effect.sync(() => { + // Discord returns the deleted template object. + expect(result.code).toBe(created.code); + expect(typeof result.name).toBe("string"); + expect(result.source_guild_id).toBe(TEST_GUILD_ID); + expect(typeof result.usage_count).toBe("number"); + expect(typeof result.creator_id).toBe("string"); + }); + }), + ); + }, + 30_000, + ); + + it( + "error - NotFound for a template code that does not exist on the guild", + async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + // A template code that does not exist on the guild yields 404 + // NotFound. Discord may also surface 403 Forbidden depending on which + // check fires first. + await runEffect( + deleteGuildTemplate({ + guild_id: TEST_GUILD_ID, + code: NON_EXISTENT_TEMPLATE_CODE, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }, + ); + + it("error - Forbidden for a guild_id the bot is not in", async () => { + // A guild_id the bot is not a member of typically yields 403 Forbidden + // (50001 Missing Access), or 404 NotFound if the route 404s before the + // permission check. + await runEffect( + deleteGuildTemplate({ + guild_id: NON_EXISTENT_GUILD_ID, + code: NON_EXISTENT_TEMPLATE_CODE, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteLobbyMember.test.ts b/packages/discord/test/deleteLobbyMember.test.ts new file mode 100644 index 000000000..26c2d4582 --- /dev/null +++ b/packages/discord/test/deleteLobbyMember.test.ts @@ -0,0 +1,121 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { addLobbyMember } from "../src/operations/addLobbyMember.ts"; +import { createLobby } from "../src/operations/createLobby.ts"; +import { deleteLobbyMember } from "../src/operations/deleteLobbyMember.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a real user_id that the bot can manage as a lobby member. +// The bot must be the lobby owner (it is, since we create the lobby here). +const TEST_LOBBY_USER_ID = process.env.DISCORD_TEST_LOBBY_USER_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_LOBBY_ID = "100000000000000000"; +const NON_EXISTENT_USER_ID = "100000000000000001"; + +describe("deleteLobbyMember", () => { + it( + "happy path - adds a member to a lobby then removes them", + async () => { + if (!TEST_LOBBY_USER_ID) { + throw new Error( + "DISCORD_TEST_LOBBY_USER_ID env var is required for the deleteLobbyMember happy path. " + + `(testRunId=${testRunId})`, + ); + } + await runEffect( + Effect.gen(function* () { + // No deleteLobby SDK operation exists, so we use a short + // idle_timeout_seconds to let Discord auto-clean up afterwards. + const lobby = yield* createLobby({ + idle_timeout_seconds: 5, + metadata: { testRunId, op: "deleteLobbyMember" }, + }); + yield* addLobbyMember({ + lobby_id: lobby.id, + user_id: TEST_LOBBY_USER_ID, + metadata: { testRunId }, + }); + const result = yield* deleteLobbyMember({ + lobby_id: lobby.id, + user_id: TEST_LOBBY_USER_ID, + }).pipe( + // If the delete fails, attempt cleanup explicitly so the test + // member doesn't linger until the lobby idle-times out. + Effect.ensuring( + deleteLobbyMember({ + lobby_id: lobby.id, + user_id: TEST_LOBBY_USER_ID, + }).pipe(Effect.ignore), + ), + ); + return yield* Effect.sync(() => { + // 204 No Content; output schema is Void. + expect(result).toBeUndefined(); + }); + }), + ); + }, + 30_000, + ); + + it( + "error - NotFound for a user_id that is not a member of the lobby", + async () => { + // Create a fresh lobby with no members and try to remove a bogus user. + // Discord returns 404 NotFound (10094 / similar) for a missing + // member, or 403 Forbidden depending on which check fires first. + await runEffect( + Effect.gen(function* () { + const lobby = yield* createLobby({ + idle_timeout_seconds: 5, + metadata: { testRunId, op: "deleteLobbyMember-nf" }, + }); + return yield* deleteLobbyMember({ + lobby_id: lobby.id, + user_id: NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ); + }), + ); + }, + 30_000, + ); + + it("error - Forbidden for a lobby_id the bot does not own", async () => { + // A lobby_id the bot's application does not own typically yields 403 + // Forbidden, or 404 NotFound if the route 404s before the ownership + // check. + await runEffect( + deleteLobbyMember({ + lobby_id: NON_EXISTENT_LOBBY_ID, + user_id: NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteMessage.test.ts b/packages/discord/test/deleteMessage.test.ts new file mode 100644 index 000000000..99b1c11fe --- /dev/null +++ b/packages/discord/test/deleteMessage.test.ts @@ -0,0 +1,105 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createMessage } from "../src/operations/createMessage.ts"; +import { deleteMessage } from "../src/operations/deleteMessage.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a text channel where the bot can post and delete its own messages. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; +const NON_EXISTENT_MESSAGE_ID = "100000000000000001"; + +describe("deleteMessage", () => { + it( + "happy path - posts a message then deletes it", + async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the deleteMessage happy path", + ); + } + await runEffect( + Effect.gen(function* () { + const msg = yield* createMessage({ + channel_id: TEST_CHANNEL_ID, + content: `distilled-delete-message-${testRunId}`, + }); + const result = yield* deleteMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe( + // If the delete fails, attempt cleanup explicitly. + Effect.ensuring( + deleteMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe(Effect.ignore), + ), + ); + return yield* Effect.sync(() => { + // 204 No Content; output schema is Void. + expect(result).toBeUndefined(); + }); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for a message_id that does not exist in the channel", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the NotFound test", + ); + } + // A snowflake-shaped message_id that does not exist in the channel + // yields 404 NotFound. Discord may also surface 403 Forbidden depending + // on which check fires first. + await runEffect( + deleteMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for a channel_id the bot cannot see", async () => { + // A snowflake-shaped channel_id the bot cannot see typically yields 403 + // Forbidden (50001 Missing Access), or 404 NotFound if the route 404s + // before the permission check. + await runEffect( + deleteMessage({ + channel_id: NON_EXISTENT_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteMyMessageReaction.test.ts b/packages/discord/test/deleteMyMessageReaction.test.ts new file mode 100644 index 000000000..6dd33fc0e --- /dev/null +++ b/packages/discord/test/deleteMyMessageReaction.test.ts @@ -0,0 +1,120 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { addMyMessageReaction } from "../src/operations/addMyMessageReaction.ts"; +import { createMessage } from "../src/operations/createMessage.ts"; +import { deleteMessage } from "../src/operations/deleteMessage.ts"; +import { deleteMyMessageReaction } from "../src/operations/deleteMyMessageReaction.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a text channel where the bot can post and react to its own +// messages. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; +const NON_EXISTENT_MESSAGE_ID = "100000000000000001"; + +// Standard Unicode emoji. +const EMOJI = "👍"; + +describe("deleteMyMessageReaction", () => { + it( + "happy path - adds the bot's own reaction then removes it", + async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the deleteMyMessageReaction happy path", + ); + } + await runEffect( + Effect.gen(function* () { + const msg = yield* createMessage({ + channel_id: TEST_CHANNEL_ID, + content: `distilled-delete-my-reaction-${testRunId}`, + }); + return yield* Effect.gen(function* () { + yield* addMyMessageReaction({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + emoji_name: EMOJI, + }); + const result = yield* deleteMyMessageReaction({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + emoji_name: EMOJI, + }); + return yield* Effect.sync(() => { + // 204 No Content; output schema is Void. + expect(result).toBeUndefined(); + }); + }).pipe( + Effect.ensuring( + deleteMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for a message_id that does not exist in the channel", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the NotFound test", + ); + } + // A snowflake-shaped message_id that does not exist in the channel + // yields 404 NotFound. Discord may also surface 403 Forbidden depending + // on which check fires first. + await runEffect( + deleteMyMessageReaction({ + channel_id: TEST_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + emoji_name: EMOJI, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for a channel_id the bot cannot see", async () => { + // A snowflake-shaped channel_id the bot cannot see typically yields 403 + // Forbidden (50001 Missing Access), or 404 NotFound if the route 404s + // before the permission check. + await runEffect( + deleteMyMessageReaction({ + channel_id: NON_EXISTENT_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + emoji_name: EMOJI, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteOriginalWebhookMessage.test.ts b/packages/discord/test/deleteOriginalWebhookMessage.test.ts new file mode 100644 index 000000000..212589629 --- /dev/null +++ b/packages/discord/test/deleteOriginalWebhookMessage.test.ts @@ -0,0 +1,112 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { deleteOriginalWebhookMessage } from "../src/operations/deleteOriginalWebhookMessage.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// "Original" here is the message produced by the most recent webhook +// execution with wait=true (or the initial message of an interaction +// followup). Creating one in-test is blocked by a codegen gap on +// executeWebhook (the `content`/`embeds`/`files`/... body fields are not +// exposed on the SDK input), so the happy path requires: +// - DISCORD_TEST_WEBHOOK_ID and DISCORD_TEST_WEBHOOK_TOKEN for a webhook +// whose original message the operator is willing to delete +// - DISCORD_TEST_ALLOW_DELETE_WEBHOOK_ORIGINAL=1 since this is destructive +const TEST_WEBHOOK_ID = process.env.DISCORD_TEST_WEBHOOK_ID; +const TEST_WEBHOOK_TOKEN = process.env.DISCORD_TEST_WEBHOOK_TOKEN; +const ALLOW_DELETE_WEBHOOK_ORIGINAL = + process.env.DISCORD_TEST_ALLOW_DELETE_WEBHOOK_ORIGINAL === "1"; + +// Snowflake-format identifier that should not match a real webhook. +const NON_EXISTENT_WEBHOOK_ID = "100000000000000000"; +// A token that is well-formed but bogus. +const NON_EXISTENT_WEBHOOK_TOKEN = `tok-${testRunId}-not-real`; + +describe("deleteOriginalWebhookMessage", () => { + it( + "happy path - deletes the original message of a real webhook", + async () => { + if ( + !TEST_WEBHOOK_ID || + !TEST_WEBHOOK_TOKEN || + !ALLOW_DELETE_WEBHOOK_ORIGINAL + ) { + throw new Error( + "DISCORD_TEST_WEBHOOK_ID, DISCORD_TEST_WEBHOOK_TOKEN and DISCORD_TEST_ALLOW_DELETE_WEBHOOK_ORIGINAL=1 are required for the deleteOriginalWebhookMessage happy path. " + + `(testRunId=${testRunId})`, + ); + } + const result = await runEffect( + deleteOriginalWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: TEST_WEBHOOK_TOKEN, + }), + ); + // 204 No Content; output schema is Void. + expect(result).toBeUndefined(); + }, + 30_000, + ); + + it( + "error - NotFound for a real webhook with no original message to delete", + async () => { + if (!TEST_WEBHOOK_ID || !TEST_WEBHOOK_TOKEN) { + throw new Error( + "DISCORD_TEST_WEBHOOK_ID and DISCORD_TEST_WEBHOOK_TOKEN env vars are required for the NotFound test", + ); + } + // After the happy-path delete (or when the webhook has never been + // executed with wait=true), the @original message no longer exists. + // Discord returns 404 NotFound (10008). Discord may also surface 403 + // Forbidden depending on which check fires first. + await runEffect( + deleteOriginalWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: TEST_WEBHOOK_TOKEN, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }, + ); + + it( + "error - Forbidden for a webhook_id / token pair that does not match", + async () => { + // Webhook routes authenticate purely via the (id, token) tuple. A + // bogus pair fails authentication. Discord typically returns 401 + // mapped to Forbidden by the SDK, or 404 NotFound if the route 404s + // before the auth check. + await runEffect( + deleteOriginalWebhookMessage({ + webhook_id: NON_EXISTENT_WEBHOOK_ID, + webhook_token: NON_EXISTENT_WEBHOOK_TOKEN, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }, + ); +}); diff --git a/packages/discord/test/deletePin.test.ts b/packages/discord/test/deletePin.test.ts new file mode 100644 index 000000000..519be303d --- /dev/null +++ b/packages/discord/test/deletePin.test.ts @@ -0,0 +1,114 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createMessage } from "../src/operations/createMessage.ts"; +import { createPin } from "../src/operations/createPin.ts"; +import { deleteMessage } from "../src/operations/deleteMessage.ts"; +import { deletePin } from "../src/operations/deletePin.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a text channel where the bot can post, pin, and unpin messages. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; +const NON_EXISTENT_MESSAGE_ID = "100000000000000001"; + +describe("deletePin", () => { + it("happy path - pins then unpins a freshly created message", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the deletePin happy path", + ); + } + await runEffect( + Effect.gen(function* () { + const msg = yield* createMessage({ + channel_id: TEST_CHANNEL_ID, + content: `distilled-discord-deletepin-${testRunId}`, + }); + yield* createPin({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe( + Effect.ensuring( + deleteMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe(Effect.ignore), + ), + ); + return yield* deletePin({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe( + Effect.tap(() => + Effect.sync(() => { + // 204 No Content; output schema is Void. + expect(true).toBe(true); + }), + ), + Effect.ensuring( + deleteMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent message_id in a real channel", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the NotFound test", + ); + } + // Discord returns 404 NotFound when the message does not exist or is not + // pinned in the channel. + await runEffect( + deletePin({ + channel_id: TEST_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when the channel cannot be seen by the bot", async () => { + // A snowflake-shaped channel_id the bot cannot access typically yields 403 + // Forbidden (50001 Missing Access), or 404 NotFound if the route 404s + // before the permission check is reached. + await runEffect( + deletePin({ + channel_id: NON_EXISTENT_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteStageInstance.test.ts b/packages/discord/test/deleteStageInstance.test.ts new file mode 100644 index 000000000..0e3c91e56 --- /dev/null +++ b/packages/discord/test/deleteStageInstance.test.ts @@ -0,0 +1,98 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createStageInstance } from "../src/operations/createStageInstance.ts"; +import { deleteStageInstance } from "../src/operations/deleteStageInstance.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a stage channel (channel type 13) where the bot is a stage +// moderator with MANAGE_CHANNELS / MUTE_MEMBERS / MOVE_MEMBERS. Operators +// must supply DISCORD_TEST_STAGE_CHANNEL_ID for the happy path. +const TEST_STAGE_CHANNEL_ID = process.env.DISCORD_TEST_STAGE_CHANNEL_ID; + +// Snowflake-format identifier that should not match a real channel. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; + +// Discord requires a topic of 1..120 chars. +const stageTopic = (suffix: string): string => + `dt-deletestage-${suffix}-${testRunId}`.slice(0, 120); + +describe("deleteStageInstance", () => { + it("happy path - creates then deletes a stage instance", async () => { + if (!TEST_STAGE_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_STAGE_CHANNEL_ID env var is required for the deleteStageInstance happy path (channel must be a stage channel)", + ); + } + await runEffect( + Effect.gen(function* () { + yield* createStageInstance({ + channel_id: TEST_STAGE_CHANNEL_ID, + topic: stageTopic("happy"), + }); + return yield* deleteStageInstance({ + channel_id: TEST_STAGE_CHANNEL_ID, + }).pipe( + Effect.tap(() => + Effect.sync(() => { + // 204 No Content; output schema is Void. + expect(true).toBe(true); + }), + ), + Effect.ensuring( + // Idempotent cleanup in case the primary delete failed. + deleteStageInstance({ + channel_id: TEST_STAGE_CHANNEL_ID, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent channel_id", async () => { + // Discord returns 404 NotFound when no stage instance exists for the + // channel; may surface as 403 Forbidden if the bot lacks visibility. + await runEffect( + deleteStageInstance({ + channel_id: NON_EXISTENT_CHANNEL_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when targeting a channel the bot cannot moderate", async () => { + // A snowflake-shaped channel_id the bot cannot see typically yields 403 + // Forbidden (50001 Missing Access), or 404 NotFound if the route 404s + // before the permission check. + await runEffect( + deleteStageInstance({ + channel_id: NON_EXISTENT_CHANNEL_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteThreadMember.test.ts b/packages/discord/test/deleteThreadMember.test.ts new file mode 100644 index 000000000..f82e8cd77 --- /dev/null +++ b/packages/discord/test/deleteThreadMember.test.ts @@ -0,0 +1,104 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { addThreadMember } from "../src/operations/addThreadMember.ts"; +import { deleteThreadMember } from "../src/operations/deleteThreadMember.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +// testRunId is unused for path-only DELETE inputs but kept for parity with +// other discord tests that include it in resource identifiers. +void testRunId; + +// A real Discord thread (channel) the bot can manage members in, and a +// real Discord user_id to add and then remove. The bot must already be a +// member of the thread (or have permission to manage it). +const TEST_THREAD_ID = process.env.DISCORD_TEST_THREAD_ID; +const TEST_USER_ID = process.env.DISCORD_TEST_USER_ID; + +// Snowflake-format identifiers that should not match real Discord resources. +const NON_EXISTENT_THREAD_ID = "100000000000000000"; +const NON_EXISTENT_USER_ID = "100000000000000001"; + +describe("deleteThreadMember", () => { + it("happy path - removes a user previously added to a thread", async () => { + if (!TEST_THREAD_ID || !TEST_USER_ID) { + throw new Error( + "DISCORD_TEST_THREAD_ID and DISCORD_TEST_USER_ID env vars are required for the deleteThreadMember happy path", + ); + } + await runEffect( + Effect.gen(function* () { + // Ensure the user is in the thread first (idempotent — Discord + // returns 204 even if they're already a member). + yield* addThreadMember({ + channel_id: TEST_THREAD_ID, + user_id: TEST_USER_ID, + }); + return yield* deleteThreadMember({ + channel_id: TEST_THREAD_ID, + user_id: TEST_USER_ID, + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + // Discord returns 204 No Content on success. + expect(result).toBeUndefined(); + }), + ), + Effect.ensuring( + // Idempotent cleanup in case the primary delete failed. + deleteThreadMember({ + channel_id: TEST_THREAD_ID, + user_id: TEST_USER_ID, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent thread channel_id", async () => { + await runEffect( + deleteThreadMember({ + channel_id: NON_EXISTENT_THREAD_ID, + user_id: TEST_USER_ID ?? NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for an unseen thread but may surface as + // Forbidden when the bot can't see the channel. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for thread bot has no access to", async () => { + await runEffect( + deleteThreadMember({ + channel_id: NON_EXISTENT_THREAD_ID, + user_id: NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // For a thread the bot is not in, Discord typically returns + // Forbidden (50001 Missing Access) but may surface as NotFound. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteUserMessageReaction.test.ts b/packages/discord/test/deleteUserMessageReaction.test.ts new file mode 100644 index 000000000..e53b3487a --- /dev/null +++ b/packages/discord/test/deleteUserMessageReaction.test.ts @@ -0,0 +1,127 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { addMyMessageReaction } from "../src/operations/addMyMessageReaction.ts"; +import { createMessage } from "../src/operations/createMessage.ts"; +import { deleteMessage } from "../src/operations/deleteMessage.ts"; +import { deleteUserMessageReaction } from "../src/operations/deleteUserMessageReaction.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a text channel where the bot can post messages and has the +// MANAGE_MESSAGES permission to delete other users' reactions. The bot's +// own user_id is needed so we can remove the reaction it added via +// addMyMessageReaction (the @me alias is not used by this endpoint). +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; +const TEST_BOT_USER_ID = process.env.DISCORD_TEST_BOT_USER_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; +const NON_EXISTENT_MESSAGE_ID = "100000000000000001"; +const NON_EXISTENT_USER_ID = "100000000000000002"; + +// Standard Unicode emoji. +const EMOJI = "👍"; + +describe("deleteUserMessageReaction", () => { + it( + "happy path - adds a reaction as the bot then removes it by user_id", + async () => { + if (!TEST_CHANNEL_ID || !TEST_BOT_USER_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID and DISCORD_TEST_BOT_USER_ID env vars are required for the deleteUserMessageReaction happy path", + ); + } + await runEffect( + Effect.gen(function* () { + const msg = yield* createMessage({ + channel_id: TEST_CHANNEL_ID, + content: `distilled-delete-user-reaction-${testRunId}`, + }); + return yield* Effect.gen(function* () { + yield* addMyMessageReaction({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + emoji_name: EMOJI, + }); + const result = yield* deleteUserMessageReaction({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + emoji_name: EMOJI, + user_id: TEST_BOT_USER_ID, + }); + return yield* Effect.sync(() => { + // 204 No Content; output schema is Void. + expect(result).toBeUndefined(); + }); + }).pipe( + Effect.ensuring( + deleteMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for a message_id that does not exist in the channel", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the NotFound test", + ); + } + // A snowflake-shaped message_id that does not exist in the channel + // yields 404 NotFound. Discord may also surface 403 Forbidden depending + // on which check fires first. + await runEffect( + deleteUserMessageReaction({ + channel_id: TEST_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + emoji_name: EMOJI, + user_id: TEST_BOT_USER_ID ?? NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for a channel_id the bot cannot see", async () => { + // A snowflake-shaped channel_id the bot cannot see typically yields 403 + // Forbidden (50001 Missing Access), or 404 NotFound if the route 404s + // before the permission check. + await runEffect( + deleteUserMessageReaction({ + channel_id: NON_EXISTENT_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + emoji_name: EMOJI, + user_id: NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteWebhook.test.ts b/packages/discord/test/deleteWebhook.test.ts new file mode 100644 index 000000000..025515092 --- /dev/null +++ b/packages/discord/test/deleteWebhook.test.ts @@ -0,0 +1,92 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createWebhook } from "../src/operations/createWebhook.ts"; +import { deleteWebhook } from "../src/operations/deleteWebhook.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a text/announcement/forum channel where the bot has +// MANAGE_WEBHOOKS. Operators must supply DISCORD_TEST_CHANNEL_ID for the +// happy path. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifier that should not match a real webhook. +const NON_EXISTENT_WEBHOOK_ID = "100000000000000000"; + +// Discord requires webhook names of 1..80 chars and disallows certain +// substrings ("clyde", "discord"). +const webhookName = (suffix: string): string => + `dt-delwh-${suffix}-${testRunId}`.slice(0, 80); + +describe("deleteWebhook", () => { + it("happy path - creates a webhook and then deletes it", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the deleteWebhook happy path", + ); + } + await runEffect( + Effect.gen(function* () { + const webhook = yield* createWebhook({ + channel_id: TEST_CHANNEL_ID, + name: webhookName("happy"), + }); + return yield* deleteWebhook({ webhook_id: webhook.id }).pipe( + Effect.tap((result) => + Effect.sync(() => { + // 204 No Content; output schema is Void. + expect(result).toBeUndefined(); + }), + ), + Effect.ensuring( + // Idempotent cleanup in case the primary delete failed. + deleteWebhook({ webhook_id: webhook.id }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent webhook_id", async () => { + // Discord returns 404 NotFound (10015 — Unknown Webhook) for an + // unknown webhook_id. May surface as 403 Forbidden if route checks + // permissions before existence. + await runEffect( + deleteWebhook({ webhook_id: NON_EXISTENT_WEBHOOK_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when the bot cannot manage the webhook", async () => { + // A snowflake-shaped webhook_id the bot does not own typically yields + // 403 Forbidden (50013 — Missing Permissions / 50001 — Missing Access), + // or 404 NotFound if the route 404s before the permission check. + await runEffect( + deleteWebhook({ webhook_id: NON_EXISTENT_WEBHOOK_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deleteWebhookByToken.test.ts b/packages/discord/test/deleteWebhookByToken.test.ts new file mode 100644 index 000000000..5b593b25e --- /dev/null +++ b/packages/discord/test/deleteWebhookByToken.test.ts @@ -0,0 +1,125 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createWebhook } from "../src/operations/createWebhook.ts"; +import { deleteWebhook } from "../src/operations/deleteWebhook.ts"; +import { deleteWebhookByToken } from "../src/operations/deleteWebhookByToken.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a text/announcement/forum channel where the bot has +// MANAGE_WEBHOOKS so we can create a webhook and learn its token. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifier and a clearly bogus token. Real webhook +// tokens are ~68 chars of url-safe base64; an obvious junk string suffices +// to trigger NotFound / Forbidden without colliding with any real webhook. +const NON_EXISTENT_WEBHOOK_ID = "100000000000000000"; +const BOGUS_WEBHOOK_TOKEN = `not-a-real-webhook-token-${testRunId}`; + +// Discord requires webhook names of 1..80 chars and disallows certain +// substrings ("clyde", "discord"). +const webhookName = (suffix: string): string => + `dt-delwhtok-${suffix}-${testRunId}`.slice(0, 80); + +describe("deleteWebhookByToken", () => { + it("happy path - creates a webhook then deletes it via its token", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the deleteWebhookByToken happy path", + ); + } + await runEffect( + Effect.gen(function* () { + const webhook = yield* createWebhook({ + channel_id: TEST_CHANNEL_ID, + name: webhookName("happy"), + }); + if (!webhook.token) { + throw new Error( + "createWebhook did not return a token — cannot exercise deleteWebhookByToken", + ); + } + return yield* deleteWebhookByToken({ + webhook_id: webhook.id, + webhook_token: webhook.token, + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + // 204 No Content; output schema is Void. + expect(result).toBeUndefined(); + }), + ), + Effect.ensuring( + // Idempotent cleanup via the bot-auth route in case the + // by-token delete failed. + deleteWebhook({ webhook_id: webhook.id }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent webhook_id", async () => { + // Discord returns 404 NotFound (10015 — Unknown Webhook) for an + // unknown webhook_id; may surface as 403 Forbidden depending on which + // check fires first. + await runEffect( + deleteWebhookByToken({ + webhook_id: NON_EXISTENT_WEBHOOK_ID, + webhook_token: BOGUS_WEBHOOK_TOKEN, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for an invalid webhook token", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the Forbidden test (need a real webhook_id whose token mismatches)", + ); + } + // Create a real webhook so the webhook_id resolves, then call + // deleteWebhookByToken with a wrong token. Discord typically returns + // 401/403 for an invalid token; the typed error surfaces as Forbidden + // (or NotFound if Discord opts to mask existence). + await runEffect( + Effect.gen(function* () { + const webhook = yield* createWebhook({ + channel_id: TEST_CHANNEL_ID, + name: webhookName("fb"), + }); + return yield* deleteWebhookByToken({ + webhook_id: webhook.id, + webhook_token: BOGUS_WEBHOOK_TOKEN, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + Effect.ensuring( + deleteWebhook({ webhook_id: webhook.id }).pipe(Effect.ignore), + ), + ); + }), + ); + }); +}); diff --git a/packages/discord/test/deleteWebhookMessage.test.ts b/packages/discord/test/deleteWebhookMessage.test.ts new file mode 100644 index 000000000..cdf3b3a30 --- /dev/null +++ b/packages/discord/test/deleteWebhookMessage.test.ts @@ -0,0 +1,112 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { deleteWebhookMessage } from "../src/operations/deleteWebhookMessage.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Creating a webhook message in-test is blocked by a codegen gap on +// executeWebhook (the `content`/`embeds`/`files`/... body fields are not +// exposed on the SDK input), so the happy path requires the operator to +// pre-create a webhook message and supply: +// - DISCORD_TEST_WEBHOOK_ID +// - DISCORD_TEST_WEBHOOK_TOKEN +// - DISCORD_TEST_WEBHOOK_MESSAGE_ID — a message id produced by executing +// this webhook (e.g. via Discord's UI or curl with `?wait=true`) +// - DISCORD_TEST_ALLOW_DELETE_WEBHOOK_MESSAGE=1 since this is destructive +const TEST_WEBHOOK_ID = process.env.DISCORD_TEST_WEBHOOK_ID; +const TEST_WEBHOOK_TOKEN = process.env.DISCORD_TEST_WEBHOOK_TOKEN; +const TEST_WEBHOOK_MESSAGE_ID = process.env.DISCORD_TEST_WEBHOOK_MESSAGE_ID; +const ALLOW_DELETE_WEBHOOK_MESSAGE = + process.env.DISCORD_TEST_ALLOW_DELETE_WEBHOOK_MESSAGE === "1"; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_WEBHOOK_ID = "100000000000000000"; +const NON_EXISTENT_MESSAGE_ID = "100000000000000001"; +// A token that is well-formed but bogus. +const NON_EXISTENT_WEBHOOK_TOKEN = `tok-${testRunId}-not-real`; + +describe("deleteWebhookMessage", () => { + it( + "happy path - deletes a webhook message by id", + async () => { + if ( + !TEST_WEBHOOK_ID || + !TEST_WEBHOOK_TOKEN || + !TEST_WEBHOOK_MESSAGE_ID || + !ALLOW_DELETE_WEBHOOK_MESSAGE + ) { + throw new Error( + "DISCORD_TEST_WEBHOOK_ID, DISCORD_TEST_WEBHOOK_TOKEN, DISCORD_TEST_WEBHOOK_MESSAGE_ID and DISCORD_TEST_ALLOW_DELETE_WEBHOOK_MESSAGE=1 are required for the deleteWebhookMessage happy path. " + + `(testRunId=${testRunId})`, + ); + } + const result = await runEffect( + deleteWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: TEST_WEBHOOK_TOKEN, + message_id: TEST_WEBHOOK_MESSAGE_ID, + }), + ); + // 204 No Content; output schema is Void. + expect(result).toBeUndefined(); + }, + 30_000, + ); + + it("error - NotFound for non-existent message_id on a real webhook", async () => { + if (!TEST_WEBHOOK_ID || !TEST_WEBHOOK_TOKEN) { + throw new Error( + "DISCORD_TEST_WEBHOOK_ID and DISCORD_TEST_WEBHOOK_TOKEN env vars are required for the NotFound test", + ); + } + // A snowflake-shaped message_id that does not belong to this webhook + // yields 404 NotFound (10008 — Unknown Message). Discord may also + // surface 403 Forbidden depending on which check fires first. + await runEffect( + deleteWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: TEST_WEBHOOK_TOKEN, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for a webhook_id / token pair that does not match", async () => { + // Webhook routes authenticate purely via the (id, token) tuple. A + // bogus pair fails authentication. Discord typically returns 401 + // mapped to Forbidden by the SDK, or 404 NotFound if the route 404s + // before the auth check. + await runEffect( + deleteWebhookMessage({ + webhook_id: NON_EXISTENT_WEBHOOK_ID, + webhook_token: NON_EXISTENT_WEBHOOK_TOKEN, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deprecatedCreatePin.test.ts b/packages/discord/test/deprecatedCreatePin.test.ts new file mode 100644 index 000000000..65917c770 --- /dev/null +++ b/packages/discord/test/deprecatedCreatePin.test.ts @@ -0,0 +1,137 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createMessage } from "../src/operations/createMessage.ts"; +import { deleteMessage } from "../src/operations/deleteMessage.ts"; +import { deprecatedCreatePin } from "../src/operations/deprecatedCreatePin.ts"; +import { deprecatedDeletePin } from "../src/operations/deprecatedDeletePin.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a text channel where the bot can post and pin messages. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; +const NON_EXISTENT_MESSAGE_ID = "100000000000000001"; + +describe("deprecatedCreatePin", () => { + it("happy path - pins a freshly created message via the legacy route", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the deprecatedCreatePin happy path", + ); + } + await runEffect( + Effect.gen(function* () { + const msg = yield* createMessage({ + channel_id: TEST_CHANNEL_ID, + content: `distilled-deprecated-pin-${testRunId}`, + }); + return yield* deprecatedCreatePin({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + // 204 No Content; output schema is Void. + expect(result).toBeUndefined(); + }), + ), + Effect.ensuring( + deprecatedDeletePin({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe(Effect.ignore), + ), + Effect.ensuring( + deleteMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent message_id in a real channel", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the NotFound test", + ); + } + // Discord returns 404 NotFound (10008 — message does not exist) when + // the message_id does not exist in the channel. + await runEffect( + deprecatedCreatePin({ + channel_id: TEST_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) message_id", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the BadRequest test", + ); + } + // Malformed snowflakes are typically rejected with 400 Invalid Form Body, + // but the routing layer may also classify as 404. + await runEffect( + deprecatedCreatePin({ + channel_id: TEST_CHANNEL_ID, + message_id: "not-a-snowflake", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when the channel cannot be seen by the bot", async () => { + // A snowflake-shaped channel_id the bot cannot see typically yields 403 + // Forbidden (50001 Missing Access), or 404 NotFound if the route 404s + // before the permission check. + await runEffect( + deprecatedCreatePin({ + channel_id: NON_EXISTENT_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deprecatedDeletePin.test.ts b/packages/discord/test/deprecatedDeletePin.test.ts new file mode 100644 index 000000000..449153271 --- /dev/null +++ b/packages/discord/test/deprecatedDeletePin.test.ts @@ -0,0 +1,114 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createMessage } from "../src/operations/createMessage.ts"; +import { deleteMessage } from "../src/operations/deleteMessage.ts"; +import { deprecatedCreatePin } from "../src/operations/deprecatedCreatePin.ts"; +import { deprecatedDeletePin } from "../src/operations/deprecatedDeletePin.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a text channel where the bot can post, pin, and unpin messages. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; +const NON_EXISTENT_MESSAGE_ID = "100000000000000001"; + +describe("deprecatedDeletePin", () => { + it("happy path - pins then unpins a freshly created message via the legacy route", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the deprecatedDeletePin happy path", + ); + } + await runEffect( + Effect.gen(function* () { + const msg = yield* createMessage({ + channel_id: TEST_CHANNEL_ID, + content: `distilled-deprecated-unpin-${testRunId}`, + }); + yield* deprecatedCreatePin({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe( + Effect.ensuring( + deleteMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe(Effect.ignore), + ), + ); + return yield* deprecatedDeletePin({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + // 204 No Content; output schema is Void. + expect(result).toBeUndefined(); + }), + ), + Effect.ensuring( + deleteMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent message_id in a real channel", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the NotFound test", + ); + } + // Discord returns 404 NotFound when the message does not exist or is + // not pinned in the channel. + await runEffect( + deprecatedDeletePin({ + channel_id: TEST_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when the channel cannot be seen by the bot", async () => { + // A snowflake-shaped channel_id the bot cannot access typically yields + // 403 Forbidden (50001 Missing Access), or 404 NotFound if the route + // 404s before the permission check is reached. + await runEffect( + deprecatedDeletePin({ + channel_id: NON_EXISTENT_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/deprecatedListPins.test.ts b/packages/discord/test/deprecatedListPins.test.ts new file mode 100644 index 000000000..27abf5bb6 --- /dev/null +++ b/packages/discord/test/deprecatedListPins.test.ts @@ -0,0 +1,76 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { deprecatedListPins } from "../src/operations/deprecatedListPins.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +// testRunId is unused for path-only GET inputs but kept for parity with +// other discord tests that include it in resource identifiers. +void testRunId; + +// Requires a text channel the bot can read messages from. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifier that should not match a real channel. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; + +describe("deprecatedListPins", () => { + it("happy path - lists pinned messages via the legacy route", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the deprecatedListPins happy path", + ); + } + const result = await runEffect( + deprecatedListPins({ channel_id: TEST_CHANNEL_ID }), + ); + // Discord returns an array of message objects (possibly empty). + expect(Array.isArray(result)).toBe(true); + for (const msg of result) { + expect(typeof msg.id).toBe("string"); + expect(msg.channel_id).toBe(TEST_CHANNEL_ID); + expect(msg.pinned).toBe(true); + } + }); + + it("error - NotFound for non-existent channel_id", async () => { + // Discord returns 404 NotFound (10003 — Unknown Channel) for an unknown + // channel, but may surface as 403 Forbidden when the bot can't see it. + await runEffect( + deprecatedListPins({ channel_id: NON_EXISTENT_CHANNEL_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when the channel cannot be read by the bot", async () => { + // A snowflake-shaped channel_id the bot cannot access typically yields + // 403 Forbidden (50001 Missing Access), or 404 NotFound if the route + // 404s before the permission check. + await runEffect( + deprecatedListPins({ channel_id: NON_EXISTENT_CHANNEL_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/editLobby.test.ts b/packages/discord/test/editLobby.test.ts new file mode 100644 index 000000000..b6eaa97ab --- /dev/null +++ b/packages/discord/test/editLobby.test.ts @@ -0,0 +1,123 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createLobby } from "../src/operations/createLobby.ts"; +import { editLobby } from "../src/operations/editLobby.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// editLobby uses lobby_id from the path. The happy path creates a lobby +// with createLobby, edits it, and lets Discord's idle timeout reap it +// (there is no deleteLobby operation in the SDK). + +// Snowflake-format identifier that should not match a real lobby. +const NON_EXISTENT_LOBBY_ID = "100000000000000000"; + +describe("editLobby", () => { + it("happy path - edits a freshly created lobby's metadata", async () => { + await runEffect( + Effect.gen(function* () { + const lobby = yield* createLobby({ + idle_timeout_seconds: 5, + metadata: { + distilled_test_run_id: testRunId, + distilled_phase: "before", + }, + }); + const updated = yield* editLobby({ + lobby_id: lobby.id, + metadata: { + distilled_test_run_id: testRunId, + distilled_phase: "after", + }, + }); + return yield* Effect.sync(() => { + expect(updated.id).toBe(lobby.id); + expect(typeof updated.application_id).toBe("string"); + expect(Array.isArray(updated.members)).toBe(true); + expect(typeof updated.flags).toBe("number"); + // Metadata round-trips with the new values. + expect(updated.metadata?.distilled_test_run_id).toBe(testRunId); + expect(updated.metadata?.distilled_phase).toBe("after"); + }); + }), + ); + }); + + it("error - NotFound for non-existent lobby_id", async () => { + // Discord returns 404 NotFound for an unknown lobby; may surface as + // 403 Forbidden when the application lacks visibility. + await runEffect( + editLobby({ + lobby_id: NON_EXISTENT_LOBBY_ID, + metadata: { distilled_test_run_id: testRunId }, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for negative idle_timeout_seconds", async () => { + // Discord enforces a positive bound on idle_timeout_seconds; -1 is + // rejected with 400 Invalid Form Body. We need a real lobby_id to make + // the route resolve before the body validation runs. + await runEffect( + Effect.gen(function* () { + const lobby = yield* createLobby({ + idle_timeout_seconds: 5, + metadata: { distilled_test_run_id: testRunId }, + }); + return yield* editLobby({ + lobby_id: lobby.id, + idle_timeout_seconds: -1, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ); + }), + ); + }); + + it("error - Forbidden / BadRequest when override_event_webhooks_url is malformed", async () => { + // Discord requires override_event_webhooks_url to be a fully-qualified + // HTTPS URL; a non-URL string is rejected. Some applications also lack + // the scope to set this field, in which case Discord returns 403. + await runEffect( + editLobby({ + lobby_id: NON_EXISTENT_LOBBY_ID, + override_event_webhooks_url: "not-a-valid-url", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "BadRequest", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/editLobbyChannelLink.test.ts b/packages/discord/test/editLobbyChannelLink.test.ts new file mode 100644 index 000000000..aef9504fc --- /dev/null +++ b/packages/discord/test/editLobbyChannelLink.test.ts @@ -0,0 +1,119 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createLobby } from "../src/operations/createLobby.ts"; +import { editLobbyChannelLink } from "../src/operations/editLobbyChannelLink.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// editLobbyChannelLink uses lobby_id from the path. Calling without a +// channel_id unlinks any current channel; that's the simplest happy-path +// shape since freshly created lobbies have no link. + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_LOBBY_ID = "100000000000000000"; +const NON_EXISTENT_CHANNEL_ID = "100000000000000001"; + +describe("editLobbyChannelLink", () => { + it("happy path - unlinks the channel of a freshly created lobby", async () => { + await runEffect( + Effect.gen(function* () { + const lobby = yield* createLobby({ + idle_timeout_seconds: 5, + metadata: { + distilled_test_run_id: testRunId, + }, + }); + const updated = yield* editLobbyChannelLink({ + lobby_id: lobby.id, + }); + return yield* Effect.sync(() => { + expect(updated.id).toBe(lobby.id); + expect(typeof updated.application_id).toBe("string"); + expect(Array.isArray(updated.members)).toBe(true); + expect(typeof updated.flags).toBe("number"); + // After unlinking, no channel is attached. + expect(updated.linked_channel).toBeUndefined(); + }); + }), + ); + }); + + it("error - NotFound for non-existent lobby_id", async () => { + // Discord returns 404 NotFound for an unknown lobby; may surface as + // 403 Forbidden when the application lacks visibility. + await runEffect( + editLobbyChannelLink({ + lobby_id: NON_EXISTENT_LOBBY_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for malformed channel_id type", async () => { + // The spec types channel_id as optional/loose. A clearly malformed + // value (number where Discord expects a snowflake string, or a + // non-snowflake string) is rejected with 400 Invalid Form Body. The + // route may also resolve to 404/403 first depending on lobby + // visibility. + await runEffect( + Effect.gen(function* () { + const lobby = yield* createLobby({ + idle_timeout_seconds: 5, + metadata: { distilled_test_run_id: testRunId }, + }); + return yield* editLobbyChannelLink({ + lobby_id: lobby.id, + channel_id: "not-a-snowflake", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ); + }), + ); + }); + + it("error - Forbidden when targeting a channel the application cannot link", async () => { + // Linking a channel the application does not own / cannot access + // typically yields 403 Forbidden (50001 Missing Access). The lobby + // route may also 404 first if the lobby isn't visible. + await runEffect( + editLobbyChannelLink({ + lobby_id: NON_EXISTENT_LOBBY_ID, + channel_id: NON_EXISTENT_CHANNEL_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/executeGithubCompatibleWebhook.test.ts b/packages/discord/test/executeGithubCompatibleWebhook.test.ts new file mode 100644 index 000000000..959e1f6a0 --- /dev/null +++ b/packages/discord/test/executeGithubCompatibleWebhook.test.ts @@ -0,0 +1,173 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createWebhook } from "../src/operations/createWebhook.ts"; +import { deleteWebhook } from "../src/operations/deleteWebhook.ts"; +import { executeGithubCompatibleWebhook } from "../src/operations/executeGithubCompatibleWebhook.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a text/announcement channel where the bot has MANAGE_WEBHOOKS, +// so we can create a webhook and obtain its token. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifier and a clearly bogus token. +const NON_EXISTENT_WEBHOOK_ID = "100000000000000000"; +const BOGUS_WEBHOOK_TOKEN = `not-a-real-webhook-token-${testRunId}`; + +// Discord requires webhook names of 1..80 chars and disallows certain +// substrings ("clyde", "discord"). +const webhookName = (suffix: string): string => + `dt-execgh-${suffix}-${testRunId}`.slice(0, 80); + +// Minimal GitHub-shaped sender object the spec requires. +const sender = { + id: 1, + login: `distilled-${testRunId}`, + html_url: "https://github.com/distilled-test", + avatar_url: "https://avatars.githubusercontent.com/u/1?v=4", +}; + +describe("executeGithubCompatibleWebhook", () => { + it( + "happy path - posts a github-style payload to a freshly created webhook", + async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the executeGithubCompatibleWebhook happy path", + ); + } + await runEffect( + Effect.gen(function* () { + const webhook = yield* createWebhook({ + channel_id: TEST_CHANNEL_ID, + name: webhookName("happy"), + }); + if (!webhook.token) { + throw new Error( + "createWebhook did not return a token — cannot exercise executeGithubCompatibleWebhook", + ); + } + return yield* executeGithubCompatibleWebhook({ + webhook_id: webhook.id, + webhook_token: webhook.token, + action: "created", + sender, + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + // 204 No Content; output schema is Void. + expect(result).toBeUndefined(); + }), + ), + Effect.ensuring( + deleteWebhook({ webhook_id: webhook.id }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for non-existent webhook_id", async () => { + // Discord returns 404 NotFound (10015 — Unknown Webhook) for an + // unknown webhook_id; may surface as 403 Forbidden depending on the + // route's check order. + await runEffect( + executeGithubCompatibleWebhook({ + webhook_id: NON_EXISTENT_WEBHOOK_ID, + webhook_token: BOGUS_WEBHOOK_TOKEN, + sender, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest when the github payload shape is rejected", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the BadRequest test", + ); + } + // Discord's github-compatible adapter requires a recognized event + // shape. A bare sender with no event-specific fields and no X-GitHub- + // Event header is rejected with 400 Invalid Form Body. Routing layers + // may also classify as 404/403 first. + await runEffect( + Effect.gen(function* () { + const webhook = yield* createWebhook({ + channel_id: TEST_CHANNEL_ID, + name: webhookName("br"), + }); + if (!webhook.token) { + throw new Error( + "createWebhook did not return a token — cannot exercise BadRequest", + ); + } + return yield* executeGithubCompatibleWebhook({ + webhook_id: webhook.id, + webhook_token: webhook.token, + sender: { + // Discord expects sender.id to be a number; supply an obviously + // out-of-range placeholder along with no event payload to + // trigger a 400. + id: 0, + login: "", + html_url: "", + avatar_url: "", + }, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + Effect.ensuring( + deleteWebhook({ webhook_id: webhook.id }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - Forbidden for a webhook_id / token pair that does not match", async () => { + // Webhook routes authenticate purely via the (id, token) tuple. A + // bogus pair fails authentication. Discord typically returns 401 + // mapped to Forbidden by the SDK, or 404 NotFound if the route 404s + // before the auth check. + await runEffect( + executeGithubCompatibleWebhook({ + webhook_id: NON_EXISTENT_WEBHOOK_ID, + webhook_token: BOGUS_WEBHOOK_TOKEN, + sender, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/executeSlackCompatibleWebhook.test.ts b/packages/discord/test/executeSlackCompatibleWebhook.test.ts new file mode 100644 index 000000000..eb2b4ec79 --- /dev/null +++ b/packages/discord/test/executeSlackCompatibleWebhook.test.ts @@ -0,0 +1,159 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createWebhook } from "../src/operations/createWebhook.ts"; +import { deleteWebhook } from "../src/operations/deleteWebhook.ts"; +import { executeSlackCompatibleWebhook } from "../src/operations/executeSlackCompatibleWebhook.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a text/announcement channel where the bot has MANAGE_WEBHOOKS, +// so we can create a webhook and obtain its token. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifier and a clearly bogus token. +const NON_EXISTENT_WEBHOOK_ID = "100000000000000000"; +const BOGUS_WEBHOOK_TOKEN = `not-a-real-webhook-token-${testRunId}`; + +// Discord requires webhook names of 1..80 chars and disallows certain +// substrings ("clyde", "discord"). +const webhookName = (suffix: string): string => + `dt-execslack-${suffix}-${testRunId}`.slice(0, 80); + +describe("executeSlackCompatibleWebhook", () => { + it( + "happy path - posts a slack-style payload to a freshly created webhook", + async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the executeSlackCompatibleWebhook happy path", + ); + } + await runEffect( + Effect.gen(function* () { + const webhook = yield* createWebhook({ + channel_id: TEST_CHANNEL_ID, + name: webhookName("happy"), + }); + if (!webhook.token) { + throw new Error( + "createWebhook did not return a token — cannot exercise executeSlackCompatibleWebhook", + ); + } + return yield* executeSlackCompatibleWebhook({ + webhook_id: webhook.id, + webhook_token: webhook.token, + text: `distilled-slack-${testRunId}`, + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + // Discord returns either an "ok" body or null/empty + // depending on the wait flag. Both shapes are accepted by + // the NullOr(String) schema. + if (result !== null) { + expect(typeof result).toBe("string"); + } + }), + ), + Effect.ensuring( + deleteWebhook({ webhook_id: webhook.id }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for non-existent webhook_id", async () => { + // Discord returns 404 NotFound (10015 — Unknown Webhook) for an + // unknown webhook_id; may surface as 403 Forbidden depending on the + // route's check order. + await runEffect( + executeSlackCompatibleWebhook({ + webhook_id: NON_EXISTENT_WEBHOOK_ID, + webhook_token: BOGUS_WEBHOOK_TOKEN, + text: `distilled-slack-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest when the slack payload has no content", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the BadRequest test", + ); + } + // Discord's slack-compatible adapter requires a text or attachments + // field — a payload with neither is rejected with 400 Invalid Form + // Body. Routing layers may also classify as 404/403 first. + await runEffect( + Effect.gen(function* () { + const webhook = yield* createWebhook({ + channel_id: TEST_CHANNEL_ID, + name: webhookName("br"), + }); + if (!webhook.token) { + throw new Error( + "createWebhook did not return a token — cannot exercise BadRequest", + ); + } + return yield* executeSlackCompatibleWebhook({ + webhook_id: webhook.id, + webhook_token: webhook.token, + // No text and no attachments — empty payload should be rejected. + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + Effect.ensuring( + deleteWebhook({ webhook_id: webhook.id }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - Forbidden for a webhook_id / token pair that does not match", async () => { + // Webhook routes authenticate purely via the (id, token) tuple. A + // bogus pair fails authentication. Discord typically returns 401 + // mapped to Forbidden by the SDK, or 404 NotFound if the route 404s + // before the auth check. + await runEffect( + executeSlackCompatibleWebhook({ + webhook_id: NON_EXISTENT_WEBHOOK_ID, + webhook_token: BOGUS_WEBHOOK_TOKEN, + text: `distilled-slack-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/executeWebhook.test.ts b/packages/discord/test/executeWebhook.test.ts new file mode 100644 index 000000000..e354ba95d --- /dev/null +++ b/packages/discord/test/executeWebhook.test.ts @@ -0,0 +1,162 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createWebhook } from "../src/operations/createWebhook.ts"; +import { deleteWebhook } from "../src/operations/deleteWebhook.ts"; +import { executeWebhook } from "../src/operations/executeWebhook.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a text/announcement channel where the bot has MANAGE_WEBHOOKS, +// so we can create a webhook and obtain its token. The happy path +// additionally requires DISCORD_TEST_ALLOW_EXECUTE_WEBHOOK=1 because the +// SDK input schema for executeWebhook is missing the body fields +// (content/embeds/components/...), so a successful run requires the +// operator's confirmation that the codegen gap has been worked around +// (e.g. by patching the spec or sending body content out of band). +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; +const ALLOW_EXECUTE_WEBHOOK = + process.env.DISCORD_TEST_ALLOW_EXECUTE_WEBHOOK === "1"; + +// Snowflake-format identifier and a clearly bogus token. +const NON_EXISTENT_WEBHOOK_ID = "100000000000000000"; +const BOGUS_WEBHOOK_TOKEN = `not-a-real-webhook-token-${testRunId}`; + +// Discord requires webhook names of 1..80 chars and disallows certain +// substrings ("clyde", "discord"). +const webhookName = (suffix: string): string => + `dt-execwh-${suffix}-${testRunId}`.slice(0, 80); + +describe("executeWebhook", () => { + it( + "happy path - executes a freshly created webhook with wait=true", + async () => { + if (!TEST_CHANNEL_ID || !ALLOW_EXECUTE_WEBHOOK) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID and DISCORD_TEST_ALLOW_EXECUTE_WEBHOOK=1 are required for the executeWebhook happy path. " + + "The SDK input is missing body fields (content/embeds/components/...) due to a codegen gap; the happy path is gated until the spec is patched.", + ); + } + await runEffect( + Effect.gen(function* () { + const webhook = yield* createWebhook({ + channel_id: TEST_CHANNEL_ID, + name: webhookName("happy"), + }); + if (!webhook.token) { + throw new Error( + "createWebhook did not return a token — cannot exercise executeWebhook", + ); + } + return yield* executeWebhook({ + webhook_id: webhook.id, + webhook_token: webhook.token, + wait: true, + }).pipe( + Effect.tap((message) => + Effect.sync(() => { + expect(typeof message.id).toBe("string"); + expect(message.id.length).toBeGreaterThan(0); + expect(typeof message.channel_id).toBe("string"); + expect(typeof message.content).toBe("string"); + }), + ), + Effect.ensuring( + deleteWebhook({ webhook_id: webhook.id }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for non-existent webhook_id", async () => { + // Discord returns 404 NotFound (10015 — Unknown Webhook) for an + // unknown webhook_id; may surface as 403 Forbidden depending on the + // route's check order. + await runEffect( + executeWebhook({ + webhook_id: NON_EXISTENT_WEBHOOK_ID, + webhook_token: BOGUS_WEBHOOK_TOKEN, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest when executing a real webhook without body content", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the BadRequest test", + ); + } + // Discord requires content / embeds / components / sticker_ids / poll + // / files. The SDK input has none of those exposed, so calling + // against a real webhook produces 400 Invalid Form Body. + await runEffect( + Effect.gen(function* () { + const webhook = yield* createWebhook({ + channel_id: TEST_CHANNEL_ID, + name: webhookName("br"), + }); + if (!webhook.token) { + throw new Error( + "createWebhook did not return a token — cannot exercise BadRequest", + ); + } + return yield* executeWebhook({ + webhook_id: webhook.id, + webhook_token: webhook.token, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + Effect.ensuring( + deleteWebhook({ webhook_id: webhook.id }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - Forbidden for a webhook_id / token pair that does not match", async () => { + // Webhook routes authenticate purely via the (id, token) tuple. A + // bogus pair fails authentication. Discord typically returns 401 + // mapped to Forbidden by the SDK, or 404 NotFound if the route 404s + // before the auth check. + await runEffect( + executeWebhook({ + webhook_id: NON_EXISTENT_WEBHOOK_ID, + webhook_token: BOGUS_WEBHOOK_TOKEN, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/followChannel.test.ts b/packages/discord/test/followChannel.test.ts new file mode 100644 index 000000000..e21800770 --- /dev/null +++ b/packages/discord/test/followChannel.test.ts @@ -0,0 +1,137 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { deleteWebhook } from "../src/operations/deleteWebhook.ts"; +import { followChannel } from "../src/operations/followChannel.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +// testRunId is unused for path-only POST inputs but kept for parity with +// other discord tests that include it in resource identifiers. +void testRunId; + +// followChannel mirrors an announcement channel (type 5 — "news") into a +// destination text channel by creating a webhook in the destination. The +// happy path requires: +// - DISCORD_TEST_ANNOUNCEMENT_CHANNEL_ID — source announcement channel +// - DISCORD_TEST_CHANNEL_ID — destination text channel where the bot has +// MANAGE_WEBHOOKS +const TEST_ANNOUNCEMENT_CHANNEL_ID = + process.env.DISCORD_TEST_ANNOUNCEMENT_CHANNEL_ID; +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifiers that should not match real channels. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; +const NON_EXISTENT_WEBHOOK_TARGET_ID = "100000000000000001"; + +describe("followChannel", () => { + it( + "happy path - follows an announcement channel into a destination text channel", + async () => { + if (!TEST_ANNOUNCEMENT_CHANNEL_ID || !TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_ANNOUNCEMENT_CHANNEL_ID and DISCORD_TEST_CHANNEL_ID env vars are required for the followChannel happy path", + ); + } + await runEffect( + Effect.gen(function* () { + const follower = yield* followChannel({ + channel_id: TEST_ANNOUNCEMENT_CHANNEL_ID, + webhook_channel_id: TEST_CHANNEL_ID, + }); + return yield* Effect.sync(() => { + expect(follower.channel_id).toBe(TEST_ANNOUNCEMENT_CHANNEL_ID); + expect(typeof follower.webhook_id).toBe("string"); + expect(follower.webhook_id.length).toBeGreaterThan(0); + }).pipe( + Effect.ensuring( + // Delete the mirror webhook Discord just created in the + // destination channel. + deleteWebhook({ webhook_id: follower.webhook_id }).pipe( + Effect.ignore, + ), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for non-existent source channel_id", async () => { + // Discord returns 404 NotFound for an unknown source channel; may + // surface as 403 Forbidden when the bot can't see it. + await runEffect( + followChannel({ + channel_id: NON_EXISTENT_CHANNEL_ID, + webhook_channel_id: TEST_CHANNEL_ID ?? NON_EXISTENT_WEBHOOK_TARGET_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest when the source is not an announcement channel", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the BadRequest test", + ); + } + // Following a channel that is not type 5 (Announcement) is rejected + // with 400 Invalid Form Body. Routing layers may also classify as + // 404/403 first. + await runEffect( + followChannel({ + channel_id: TEST_CHANNEL_ID, + webhook_channel_id: TEST_CHANNEL_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when the destination channel cannot be managed by the bot", async () => { + // A snowflake-shaped webhook_channel_id the bot does not own / + // cannot manage typically yields 403 Forbidden (50013 — Missing + // Permissions / 50001 — Missing Access). May surface as 404 NotFound + // if the route 404s before the permission check. + await runEffect( + followChannel({ + channel_id: + TEST_ANNOUNCEMENT_CHANNEL_ID ?? NON_EXISTENT_CHANNEL_ID, + webhook_channel_id: NON_EXISTENT_WEBHOOK_TARGET_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getActiveGuildThreads.test.ts b/packages/discord/test/getActiveGuildThreads.test.ts new file mode 100644 index 000000000..1097d350b --- /dev/null +++ b/packages/discord/test/getActiveGuildThreads.test.ts @@ -0,0 +1,77 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { getActiveGuildThreads } from "../src/operations/getActiveGuildThreads.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +// testRunId is unused for path-only GET inputs but kept for parity with +// other discord tests that include it in resource identifiers. +void testRunId; + +// Requires a guild the bot is in and can read threads from. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifier that should not match a real guild. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; + +describe("getActiveGuildThreads", () => { + it("happy path - lists active threads in a real guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the getActiveGuildThreads happy path", + ); + } + const result = await runEffect( + getActiveGuildThreads({ guild_id: TEST_GUILD_ID }), + ); + expect(Array.isArray(result.threads)).toBe(true); + for (const thread of result.threads) { + expect(typeof thread.id).toBe("string"); + expect(thread.guild_id).toBe(TEST_GUILD_ID); + expect(typeof thread.name).toBe("string"); + // Active means not archived per Discord's definition. + expect(thread.thread_metadata.archived).toBe(false); + } + }); + + it("error - NotFound for non-existent guild_id", async () => { + // Discord returns 404 NotFound for an unknown guild; may surface as + // 403 Forbidden when the bot can't see it. + await runEffect( + getActiveGuildThreads({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for a guild the bot is not in", async () => { + // A snowflake-shaped guild_id the bot is not a member of typically + // yields 403 Forbidden (50001 — Missing Access), or 404 NotFound if + // the route 404s before the membership check. + await runEffect( + getActiveGuildThreads({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getAnswerVoters.test.ts b/packages/discord/test/getAnswerVoters.test.ts new file mode 100644 index 000000000..562c61cda --- /dev/null +++ b/packages/discord/test/getAnswerVoters.test.ts @@ -0,0 +1,123 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createMessage } from "../src/operations/createMessage.ts"; +import { deleteMessage } from "../src/operations/deleteMessage.ts"; +import { getAnswerVoters } from "../src/operations/getAnswerVoters.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a text channel where the bot can post messages. The happy path +// creates a poll-bearing message and reads its voters; the bot must be +// able to post polls (CREATE_POLLS / SEND_MESSAGES). +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; +const NON_EXISTENT_MESSAGE_ID = "100000000000000001"; + +describe("getAnswerVoters", () => { + it( + "happy path - lists voters for a fresh poll's first answer", + async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the getAnswerVoters happy path", + ); + } + await runEffect( + Effect.gen(function* () { + const msg = yield* createMessage({ + channel_id: TEST_CHANNEL_ID, + poll: { + question: { text: `distilled-poll-${testRunId}` }, + answers: [ + { poll_media: { text: "yes" } }, + { poll_media: { text: "no" } }, + ], + duration: 1, + allow_multiselect: false, + layout_type: 1, + }, + }); + return yield* getAnswerVoters({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + answer_id: 1, + }).pipe( + Effect.tap((result) => + Effect.sync(() => { + expect(Array.isArray(result.users)).toBe(true); + for (const user of result.users) { + expect(typeof user.id).toBe("string"); + expect(typeof user.username).toBe("string"); + } + }), + ), + Effect.ensuring( + deleteMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for non-existent message_id in a real channel", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the NotFound test", + ); + } + // Discord returns 404 NotFound for an unknown message; may surface as + // 403 Forbidden depending on which check fires first. + await runEffect( + getAnswerVoters({ + channel_id: TEST_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + answer_id: 1, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for a channel_id the bot cannot see", async () => { + // A snowflake-shaped channel_id the bot cannot access typically yields + // 403 Forbidden (50001 — Missing Access), or 404 NotFound if the route + // 404s before the permission check. + await runEffect( + getAnswerVoters({ + channel_id: NON_EXISTENT_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + answer_id: 1, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getApplication.test.ts b/packages/discord/test/getApplication.test.ts new file mode 100644 index 000000000..12ac7702d --- /dev/null +++ b/packages/discord/test/getApplication.test.ts @@ -0,0 +1,72 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { getApplication } from "../src/operations/getApplication.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +// testRunId is unused for path-only GET inputs but kept for parity with +// other discord tests that include it in resource identifiers. +void testRunId; + +// The "@me" alias resolves to the application owning the bot token. +const SELF_APPLICATION_ID = "@me"; + +// Snowflake-format identifier that should not match a real application. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; + +describe("getApplication", () => { + it("happy path - fetches the bot's own application via @me", async () => { + const result = await runEffect( + getApplication({ application_id: SELF_APPLICATION_ID }), + ); + expect(typeof result.id).toBe("string"); + expect(result.id.length).toBeGreaterThan(0); + expect(typeof result.name).toBe("string"); + expect(typeof result.description).toBe("string"); + }); + + it("error - NotFound for non-existent application_id", async () => { + // Discord returns 404 NotFound for an unknown application_id; may + // surface as 403 Forbidden depending on which check fires first. + await runEffect( + getApplication({ + application_id: NON_EXISTENT_APPLICATION_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for an application_id the bot does not own", async () => { + // Bots can only fetch their own application. Querying another + // application typically yields 403 Forbidden, or 404 NotFound when + // Discord opts to mask existence. + await runEffect( + getApplication({ + application_id: NON_EXISTENT_APPLICATION_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getApplicationCommand.test.ts b/packages/discord/test/getApplicationCommand.test.ts new file mode 100644 index 000000000..acf261a2a --- /dev/null +++ b/packages/discord/test/getApplicationCommand.test.ts @@ -0,0 +1,109 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createApplicationCommand } from "../src/operations/createApplicationCommand.ts"; +import { deleteApplicationCommand } from "../src/operations/deleteApplicationCommand.ts"; +import { getApplicationCommand } from "../src/operations/getApplicationCommand.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint requires: +// - the bot's application_id (snowflake) — the bot's token must own it. +// - the command_id (snowflake) of a command that belongs to that application. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; + +// Snowflake-format identifier that should not match a real application/command. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; +const NON_EXISTENT_COMMAND_ID = "100000000000000001"; + +// Discord requires CHAT_INPUT command names to match ^[-_\p{L}\p{N}]{1,32}$. +const cmdName = (suffix: string): string => + `dtest-${suffix}-${testRunId}`.toLowerCase().slice(0, 32); + +describe("getApplicationCommand", () => { + it("happy path - fetches a freshly created application command by id", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the getApplicationCommand happy path", + ); + } + const name = cmdName("get"); + await runEffect( + Effect.gen(function* () { + const created = yield* createApplicationCommand({ + application_id: TEST_APPLICATION_ID, + name, + description: "distilled test command", + }); + return yield* Effect.gen(function* () { + const fetched = yield* getApplicationCommand({ + application_id: TEST_APPLICATION_ID, + command_id: created.id, + }); + expect(fetched.id).toBe(created.id); + expect(fetched.application_id).toBe(TEST_APPLICATION_ID); + expect(fetched.name).toBe(name); + expect(fetched.description).toBe("distilled test command"); + expect(typeof fetched.version).toBe("string"); + }).pipe( + Effect.ensuring( + deleteApplicationCommand({ + application_id: TEST_APPLICATION_ID, + command_id: created.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent command_id under the bot's application", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the NotFound test", + ); + } + await runEffect( + getApplicationCommand({ + application_id: TEST_APPLICATION_ID, + command_id: NON_EXISTENT_COMMAND_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for application_id the bot does not own", async () => { + // Looking up a command under an application_id the bot's token does not + // own typically yields 403 Forbidden; may also surface as 404 NotFound + // when the route resolves the application before the permission check. + await runEffect( + getApplicationCommand({ + application_id: NON_EXISTENT_APPLICATION_ID, + command_id: NON_EXISTENT_COMMAND_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getApplicationEmoji.test.ts b/packages/discord/test/getApplicationEmoji.test.ts new file mode 100644 index 000000000..e3d996974 --- /dev/null +++ b/packages/discord/test/getApplicationEmoji.test.ts @@ -0,0 +1,118 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createApplicationEmoji } from "../src/operations/createApplicationEmoji.ts"; +import { deleteApplicationEmoji } from "../src/operations/deleteApplicationEmoji.ts"; +import { getApplicationEmoji } from "../src/operations/getApplicationEmoji.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Smallest valid 1x1 transparent PNG, encoded as a data URI. Discord accepts +// data URIs of the form "data:image/{png,jpeg,gif};base64,...". +const TINY_PNG_DATA_URI = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII="; + +// The endpoint requires: +// - the bot's application_id (snowflake) — the bot's token must own it. +// - the emoji_id (snowflake) of an emoji owned by that application. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; + +// Snowflake-format identifiers that should not match real resources. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; +const NON_EXISTENT_EMOJI_ID = "100000000000000001"; + +// Discord requires emoji names to match ^[a-zA-Z0-9_]{2,32}$. +const emojiName = (suffix: string): string => { + const raw = `dt_${suffix}_${testRunId}`; + return raw.replace(/[^a-zA-Z0-9_]/g, "_").slice(0, 32); +}; + +describe("getApplicationEmoji", () => { + it("happy path - fetches a freshly created application emoji by id", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the getApplicationEmoji happy path", + ); + } + const name = emojiName("get"); + await runEffect( + Effect.gen(function* () { + const created = yield* createApplicationEmoji({ + application_id: TEST_APPLICATION_ID, + name, + image: TINY_PNG_DATA_URI, + }); + return yield* Effect.gen(function* () { + const fetched = yield* getApplicationEmoji({ + application_id: TEST_APPLICATION_ID, + emoji_id: created.id, + }); + expect(fetched.id).toBe(created.id); + expect(fetched.name).toBe(name); + expect(Array.isArray(fetched.roles)).toBe(true); + expect(typeof fetched.require_colons).toBe("boolean"); + expect(typeof fetched.managed).toBe("boolean"); + expect(typeof fetched.animated).toBe("boolean"); + expect(typeof fetched.available).toBe("boolean"); + }).pipe( + Effect.ensuring( + deleteApplicationEmoji({ + application_id: TEST_APPLICATION_ID, + emoji_id: created.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent emoji_id under the bot's application", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the NotFound test", + ); + } + await runEffect( + getApplicationEmoji({ + application_id: TEST_APPLICATION_ID, + emoji_id: NON_EXISTENT_EMOJI_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for application_id the bot does not own", async () => { + // Looking up an emoji under an application_id the bot's token does not + // own typically yields 403 Forbidden; may also surface as 404 NotFound + // when the route resolves the application before the permission check. + await runEffect( + getApplicationEmoji({ + application_id: NON_EXISTENT_APPLICATION_ID, + emoji_id: NON_EXISTENT_EMOJI_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getApplicationRoleConnectionsMetadata.test.ts b/packages/discord/test/getApplicationRoleConnectionsMetadata.test.ts new file mode 100644 index 000000000..98db943c5 --- /dev/null +++ b/packages/discord/test/getApplicationRoleConnectionsMetadata.test.ts @@ -0,0 +1,82 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getApplicationRoleConnectionsMetadata } from "../src/operations/getApplicationRoleConnectionsMetadata.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +// The endpoint requires: +// - the bot's application_id (snowflake) — the bot's token must own it. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; + +// Snowflake-format identifier that should not match a real application. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; + +describe("getApplicationRoleConnectionsMetadata", () => { + it("happy path - fetches role connections metadata for the bot's application", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the getApplicationRoleConnectionsMetadata happy path", + ); + } + const result = await runEffect( + getApplicationRoleConnectionsMetadata({ + application_id: TEST_APPLICATION_ID, + }), + ); + // Discord returns an array (possibly empty) of role connection metadata + // records. Assert the array shape and per-record fields when present. + expect(Array.isArray(result)).toBe(true); + for (const record of result) { + expect(typeof record.key).toBe("string"); + expect(typeof record.name).toBe("string"); + expect(typeof record.description).toBe("string"); + } + }); + + it("error - NotFound for non-existent application_id", async () => { + await runEffect( + getApplicationRoleConnectionsMetadata({ + application_id: NON_EXISTENT_APPLICATION_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for an unseen application_id, but may + // surface as Forbidden when the bot's token does not own it. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for application_id the bot does not own", async () => { + // Calling against an application_id the bot's token does not own + // typically yields 403 Forbidden; may also surface as 404 NotFound when + // the route resolves the application before the permission check. + await runEffect( + getApplicationRoleConnectionsMetadata({ + application_id: NON_EXISTENT_APPLICATION_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getApplicationUserRoleConnection.test.ts b/packages/discord/test/getApplicationUserRoleConnection.test.ts new file mode 100644 index 000000000..a3b113ac5 --- /dev/null +++ b/packages/discord/test/getApplicationUserRoleConnection.test.ts @@ -0,0 +1,87 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getApplicationUserRoleConnection } from "../src/operations/getApplicationUserRoleConnection.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +// The endpoint is /users/@me/applications/{application_id}/role-connection +// and requires a user OAuth2 bearer token with the `role_connections.write` +// scope (set DISCORD_BEARER_TOKEN). Bot tokens cannot use it. Operators must +// opt in with DISCORD_TEST_APPLICATION_ID for the happy path to run. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; + +// Snowflake-format identifier that should not match a real application. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; + +describe("getApplicationUserRoleConnection", () => { + it("happy path - fetches the calling user's role connection on the application", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID is required for the getApplicationUserRoleConnection happy path. The endpoint requires a user OAuth2 bearer token (DISCORD_BEARER_TOKEN) with role_connections.write scope.", + ); + } + const result = await runEffect( + getApplicationUserRoleConnection({ + application_id: TEST_APPLICATION_ID, + }), + ); + // The response is an ApplicationRoleConnection object — all fields are + // optional. Assert the type shape when fields are present. + if (result.platform_name !== undefined) { + expect(typeof result.platform_name).toBe("string"); + } + if (result.platform_username !== undefined && result.platform_username !== null) { + expect(typeof result.platform_username).toBe("string"); + } + if (result.metadata !== undefined) { + expect(typeof result.metadata).toBe("object"); + } + }); + + it("error - NotFound for non-existent application_id", async () => { + // Discord returns 404 NotFound for application_ids that do not exist or + // have no role connection for the calling user. + await runEffect( + getApplicationUserRoleConnection({ + application_id: NON_EXISTENT_APPLICATION_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when the token lacks role_connections.write scope", async () => { + // Bot tokens cannot use this endpoint — Discord returns 403 Forbidden + // (or 401 in some configurations). User OAuth2 tokens missing the + // role_connections.write scope return 403. May also surface as 404. + await runEffect( + getApplicationUserRoleConnection({ + application_id: NON_EXISTENT_APPLICATION_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getAutoModerationRule.test.ts b/packages/discord/test/getAutoModerationRule.test.ts new file mode 100644 index 000000000..42821c47c --- /dev/null +++ b/packages/discord/test/getAutoModerationRule.test.ts @@ -0,0 +1,98 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getAutoModerationRule } from "../src/operations/getAutoModerationRule.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +// The endpoint requires: +// - a guild the bot is in with MANAGE_GUILD permission. +// - the snowflake of an auto-moderation rule that already exists in that +// guild. The SDK's createAutoModerationRule does not currently surface +// the body schema (codegen gap), so the happy path requires the operator +// to supply an existing rule_id via DISCORD_TEST_AUTO_MODERATION_RULE_ID. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_AUTO_MODERATION_RULE_ID = + process.env.DISCORD_TEST_AUTO_MODERATION_RULE_ID; + +// Snowflake-format identifiers that should not match real resources. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_RULE_ID = "100000000000000001"; + +describe("getAutoModerationRule", () => { + it("happy path - fetches an auto-moderation rule by id", async () => { + if (!TEST_GUILD_ID || !TEST_AUTO_MODERATION_RULE_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_AUTO_MODERATION_RULE_ID env vars are required for the getAutoModerationRule happy path", + ); + } + const result = await runEffect( + getAutoModerationRule({ + guild_id: TEST_GUILD_ID, + rule_id: TEST_AUTO_MODERATION_RULE_ID, + }), + ); + // The output is typed as an opaque value because the spec does not + // describe the response body. Cast for assertions. + const rule = result as { id?: string; guild_id?: string }; + expect(typeof rule).toBe("object"); + expect(rule).not.toBeNull(); + if (rule.id !== undefined) { + expect(rule.id).toBe(TEST_AUTO_MODERATION_RULE_ID); + } + if (rule.guild_id !== undefined) { + expect(rule.guild_id).toBe(TEST_GUILD_ID); + } + }); + + it("error - NotFound for non-existent rule_id under a real guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + await runEffect( + getAutoModerationRule({ + guild_id: TEST_GUILD_ID, + rule_id: NON_EXISTENT_RULE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + getAutoModerationRule({ + guild_id: NON_EXISTENT_GUILD_ID, + rule_id: NON_EXISTENT_RULE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getBotGateway.test.ts b/packages/discord/test/getBotGateway.test.ts new file mode 100644 index 000000000..d3cc7754c --- /dev/null +++ b/packages/discord/test/getBotGateway.test.ts @@ -0,0 +1,105 @@ +import { config } from "dotenv"; +import { Effect, Layer, Redacted } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getBotGateway } from "../src/operations/getBotGateway.ts"; +import { + Credentials, + CredentialsFromEnv, + DEFAULT_API_BASE_URL, +} from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// A layer that supplies plausible-looking but invalid credentials so the +// /gateway/bot endpoint rejects the request. /gateway/bot has no input +// parameters, so error cases (NotFound / Forbidden) can only be reached by +// manipulating the auth context — Discord rejects unknown tokens at the +// auth layer before any path-routing checks run. +const bogusCredentialsLayer = (scheme: "Bot" | "Bearer"): Layer.Layer => + Layer.succeed(Credentials, { + token: Redacted.make("MTAwMDAwMDAwMDAwMDAwMDAw.bogus.token-for-distilled-tests"), + authScheme: scheme, + apiBaseUrl: process.env.DISCORD_API_BASE_URL ?? DEFAULT_API_BASE_URL, + }); + +const runWithBogusCreds = ( + effect: Effect.Effect, + scheme: "Bot" | "Bearer", +): Promise => { + const layer = Layer.merge(bogusCredentialsLayer(scheme), FetchHttpClient.layer); + return Effect.runPromise( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + effect.pipe(Effect.provide(layer)) as Effect.Effect, + ); +}; + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +describe("getBotGateway", () => { + it("happy path - returns the WSS gateway url and session start limits", async () => { + const result = await runEffect(getBotGateway({})); + expect(typeof result.url).toBe("string"); + expect(result.url.startsWith("wss://")).toBe(true); + expect(typeof result.shards).toBe("number"); + expect(result.shards).toBeGreaterThanOrEqual(1); + expect(typeof result.session_start_limit.total).toBe("number"); + expect(typeof result.session_start_limit.remaining).toBe("number"); + expect(typeof result.session_start_limit.reset_after).toBe("number"); + expect(typeof result.session_start_limit.max_concurrency).toBe("number"); + }); + + it("error - NotFound / Forbidden surface when the route rejects the request", async () => { + // /gateway/bot has no path params — the only way to trigger the listed + // typed errors is to send an unrecognized token. Discord's gateway + // routes commonly respond with 401 for invalid tokens (mapped to + // Unauthorized) but may classify the request as 404/403 depending on + // routing. Assert that the error is one of the expected typed tags. + await runWithBogusCreds( + getBotGateway({}).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "NotFound", + "Forbidden", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + "Bot", + ); + }); + + it("error - Forbidden when an OAuth2 bearer token is used on a bot-only route", async () => { + // /gateway/bot is a bot-token-only route. Calling it with a Bearer + // credential typically yields 403 Forbidden, but Discord may also + // return 401 Unauthorized depending on token validity, or 404 if the + // route resolution falls through ahead of the permission check. + await runWithBogusCreds( + getBotGateway({}).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "Forbidden", + "NotFound", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + "Bearer", + ); + }); +}); diff --git a/packages/discord/test/getChannel.test.ts b/packages/discord/test/getChannel.test.ts new file mode 100644 index 000000000..74e630bc2 --- /dev/null +++ b/packages/discord/test/getChannel.test.ts @@ -0,0 +1,73 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getChannel } from "../src/operations/getChannel.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +// The endpoint requires: +// - the channel_id (snowflake) of a channel the bot has VIEW_CHANNEL on. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifier that should not match a real channel. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; + +describe("getChannel", () => { + it("happy path - fetches a channel by id", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the getChannel happy path", + ); + } + const result = await runEffect(getChannel({ channel_id: TEST_CHANNEL_ID })); + // The output is typed as an opaque value because the spec does not + // describe the response body. Cast for assertions. + const channel = result as { id?: string; type?: number }; + expect(typeof channel).toBe("object"); + expect(channel).not.toBeNull(); + expect(channel.id).toBe(TEST_CHANNEL_ID); + expect(typeof channel.type).toBe("number"); + }); + + it("error - NotFound for non-existent channel_id", async () => { + await runEffect( + getChannel({ channel_id: NON_EXISTENT_CHANNEL_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unseen channels, but may surface + // as Forbidden (50001 Missing Access) when the bot can't see it. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when targeting a channel the bot cannot access", async () => { + // Calling against a snowflake-shaped channel_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if + // the route resolves the channel before the permission check. + await runEffect( + getChannel({ channel_id: NON_EXISTENT_CHANNEL_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getCurrentUserApplicationEntitlements.test.ts b/packages/discord/test/getCurrentUserApplicationEntitlements.test.ts new file mode 100644 index 000000000..90e405502 --- /dev/null +++ b/packages/discord/test/getCurrentUserApplicationEntitlements.test.ts @@ -0,0 +1,85 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getCurrentUserApplicationEntitlements } from "../src/operations/getCurrentUserApplicationEntitlements.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +// The endpoint is /users/@me/applications/{application_id}/entitlements +// and requires a user OAuth2 bearer token (set DISCORD_BEARER_TOKEN) with +// an entitlements-related scope. Bot tokens cannot use it. Operators must +// opt in with DISCORD_TEST_APPLICATION_ID for the happy path to run. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; + +// Snowflake-format identifier that should not match a real application. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; + +describe("getCurrentUserApplicationEntitlements", () => { + it("happy path - lists the calling user's entitlements for the application", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the getCurrentUserApplicationEntitlements happy path. The endpoint requires a user OAuth2 bearer token (DISCORD_BEARER_TOKEN).", + ); + } + const result = await runEffect( + getCurrentUserApplicationEntitlements({ + application_id: TEST_APPLICATION_ID, + }), + ); + // Discord returns an array (possibly empty) of entitlement records. + expect(Array.isArray(result)).toBe(true); + for (const entitlement of result) { + expect(typeof entitlement.id).toBe("string"); + expect(typeof entitlement.sku_id).toBe("string"); + expect(entitlement.application_id).toBe(TEST_APPLICATION_ID); + expect(typeof entitlement.user_id).toBe("string"); + expect(typeof entitlement.deleted).toBe("boolean"); + } + }); + + it("error - NotFound for non-existent application_id", async () => { + // Discord returns 404 NotFound for application_ids that do not exist; + // may surface as Forbidden when the calling token lacks access. + await runEffect( + getCurrentUserApplicationEntitlements({ + application_id: NON_EXISTENT_APPLICATION_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when the token lacks access to entitlements for the application", async () => { + // Bot tokens cannot use this user-scoped endpoint — Discord returns + // 403 Forbidden (or 401 in some configurations). User OAuth2 tokens + // missing the proper scope return 403. May also surface as 404. + await runEffect( + getCurrentUserApplicationEntitlements({ + application_id: NON_EXISTENT_APPLICATION_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getEntitlement.test.ts b/packages/discord/test/getEntitlement.test.ts new file mode 100644 index 000000000..eb4f21503 --- /dev/null +++ b/packages/discord/test/getEntitlement.test.ts @@ -0,0 +1,92 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getEntitlement } from "../src/operations/getEntitlement.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +// The endpoint requires: +// - the bot's application_id (snowflake) — the bot's token must own it. +// - the entitlement_id (snowflake) of an existing entitlement on that +// application. Entitlements normally come from real purchases, so the +// happy path requires the operator to supply an existing entitlement_id +// via DISCORD_TEST_ENTITLEMENT_ID. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; +const TEST_ENTITLEMENT_ID = process.env.DISCORD_TEST_ENTITLEMENT_ID; + +// Snowflake-format identifiers that should not match real resources. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; +const NON_EXISTENT_ENTITLEMENT_ID = "100000000000000001"; + +describe("getEntitlement", () => { + it("happy path - fetches an entitlement by id", async () => { + if (!TEST_APPLICATION_ID || !TEST_ENTITLEMENT_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID and DISCORD_TEST_ENTITLEMENT_ID env vars are required for the getEntitlement happy path", + ); + } + const result = await runEffect( + getEntitlement({ + application_id: TEST_APPLICATION_ID, + entitlement_id: TEST_ENTITLEMENT_ID, + }), + ); + expect(result.id).toBe(TEST_ENTITLEMENT_ID); + expect(result.application_id).toBe(TEST_APPLICATION_ID); + expect(typeof result.sku_id).toBe("string"); + expect(typeof result.user_id).toBe("string"); + expect(typeof result.deleted).toBe("boolean"); + }); + + it("error - NotFound for non-existent entitlement_id under the bot's application", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the NotFound test", + ); + } + await runEffect( + getEntitlement({ + application_id: TEST_APPLICATION_ID, + entitlement_id: NON_EXISTENT_ENTITLEMENT_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for application_id the bot does not own", async () => { + // Looking up an entitlement under an application_id the bot's token + // does not own typically yields 403 Forbidden; may also surface as + // 404 NotFound when the route resolves the application before the + // permission check. + await runEffect( + getEntitlement({ + application_id: NON_EXISTENT_APPLICATION_ID, + entitlement_id: NON_EXISTENT_ENTITLEMENT_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getEntitlements.test.ts b/packages/discord/test/getEntitlements.test.ts new file mode 100644 index 000000000..62b609783 --- /dev/null +++ b/packages/discord/test/getEntitlements.test.ts @@ -0,0 +1,84 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getEntitlements } from "../src/operations/getEntitlements.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +// The endpoint requires: +// - the bot's application_id (snowflake) — the bot's token must own it. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; + +// Snowflake-format identifier that should not match a real application. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; + +describe("getEntitlements", () => { + it("happy path - lists entitlements for the bot's application", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the getEntitlements happy path", + ); + } + const result = await runEffect( + getEntitlements({ + application_id: TEST_APPLICATION_ID, + limit: 5, + }), + ); + // Discord returns an array (possibly empty) of entitlement records. + expect(Array.isArray(result)).toBe(true); + for (const entitlement of result) { + expect(typeof entitlement.id).toBe("string"); + expect(typeof entitlement.sku_id).toBe("string"); + expect(entitlement.application_id).toBe(TEST_APPLICATION_ID); + expect(typeof entitlement.user_id).toBe("string"); + expect(typeof entitlement.deleted).toBe("boolean"); + } + }); + + it("error - NotFound for non-existent application_id", async () => { + await runEffect( + getEntitlements({ + application_id: NON_EXISTENT_APPLICATION_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for an unseen application_id, but may + // surface as Forbidden when the bot's token does not own it. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for application_id the bot does not own", async () => { + // Calling against an application_id the bot's token does not own + // typically yields 403 Forbidden; may also surface as 404 NotFound when + // the route resolves the application before the permission check. + await runEffect( + getEntitlements({ + application_id: NON_EXISTENT_APPLICATION_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getGateway.test.ts b/packages/discord/test/getGateway.test.ts new file mode 100644 index 000000000..ce7362ec6 --- /dev/null +++ b/packages/discord/test/getGateway.test.ts @@ -0,0 +1,87 @@ +import { config } from "dotenv"; +import { Effect, Layer, Redacted } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getGateway } from "../src/operations/getGateway.ts"; +import { Credentials, CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// A layer that points the SDK at a Discord-shaped base URL whose route does +// not exist. /gateway has no input parameters, so error cases (NotFound / +// Forbidden) can only be reached by manipulating the request context. +const customBaseUrlLayer = (apiBaseUrl: string): Layer.Layer => + Layer.succeed(Credentials, { + token: Redacted.make("MTAwMDAwMDAwMDAwMDAwMDAw.bogus.token-for-distilled-tests"), + authScheme: "Bot" as const, + apiBaseUrl, + }); + +const runWithBaseUrl = ( + effect: Effect.Effect, + apiBaseUrl: string, +): Promise => { + const layer = Layer.merge(customBaseUrlLayer(apiBaseUrl), FetchHttpClient.layer); + return Effect.runPromise( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + effect.pipe(Effect.provide(layer)) as Effect.Effect, + ); +}; + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +describe("getGateway", () => { + it("happy path - returns the WSS gateway url", async () => { + const result = await runEffect(getGateway({})); + expect(typeof result.url).toBe("string"); + expect(result.url.startsWith("wss://")).toBe(true); + }); + + it("error - NotFound when /gateway is unrouted on the configured base URL", async () => { + // /gateway has no path params; the only realistic way to surface a 404 + // is to point the SDK at a Discord-shaped base URL with a non-existent + // API version. Discord returns 404 for /api/v999/gateway. + await runWithBaseUrl( + getGateway({}).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + "https://discord.com/api/v999", + ); + }); + + it("error - Forbidden when the host classifies the request as forbidden", async () => { + // Pointing at a host that intentionally responds 403 for unknown paths + // exercises the SDK's Forbidden mapping. Cloudflare 1001 endpoints + // return 403 for many unconfigured paths — but other hosts may return + // 404 / 400, so the typed-tag set is tolerant. + await runWithBaseUrl( + getGateway({}).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + "https://discord.com/forbidden-distilled-test", + ); + }); +}); diff --git a/packages/discord/test/getGuild.test.ts b/packages/discord/test/getGuild.test.ts new file mode 100644 index 000000000..e3237ce6e --- /dev/null +++ b/packages/discord/test/getGuild.test.ts @@ -0,0 +1,78 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getGuild } from "../src/operations/getGuild.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +// The endpoint requires: +// - the guild_id (snowflake) of a guild the bot is a member of. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifier that should not match a real guild. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; + +describe("getGuild", () => { + it("happy path - fetches a guild by id with counts", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the getGuild happy path", + ); + } + const result = await runEffect( + getGuild({ guild_id: TEST_GUILD_ID, with_counts: true }), + ); + expect(result.id).toBe(TEST_GUILD_ID); + expect(typeof result.name).toBe("string"); + expect(typeof result.owner_id).toBe("string"); + expect(Array.isArray(result.roles)).toBe(true); + expect(Array.isArray(result.emojis)).toBe(true); + expect(Array.isArray(result.stickers)).toBe(true); + expect(typeof result.system_channel_flags).toBe("number"); + if (result.approximate_member_count !== undefined && result.approximate_member_count !== null) { + expect(typeof result.approximate_member_count).toBe("number"); + } + }); + + it("error - NotFound for non-existent guild_id", async () => { + await runEffect( + getGuild({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unseen guilds, but may surface as + // Forbidden (50001 Missing Access) when the bot can't see the guild. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + getGuild({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getGuildApplicationCommand.test.ts b/packages/discord/test/getGuildApplicationCommand.test.ts new file mode 100644 index 000000000..53381e77e --- /dev/null +++ b/packages/discord/test/getGuildApplicationCommand.test.ts @@ -0,0 +1,120 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildApplicationCommand } from "../src/operations/createGuildApplicationCommand.ts"; +import { deleteGuildApplicationCommand } from "../src/operations/deleteGuildApplicationCommand.ts"; +import { getGuildApplicationCommand } from "../src/operations/getGuildApplicationCommand.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint requires: +// - the bot's application_id (snowflake) — the bot's token must own it. +// - a guild the bot is a member of (DISCORD_TEST_GUILD_ID). +// - the command_id (snowflake) of a command registered to that guild. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifiers that should not match real resources. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; +const NON_EXISTENT_GUILD_ID = "100000000000000001"; +const NON_EXISTENT_COMMAND_ID = "100000000000000002"; + +// Discord requires CHAT_INPUT command names to match ^[-_\p{L}\p{N}]{1,32}$. +const cmdName = (suffix: string): string => + `dtest-${suffix}-${testRunId}`.toLowerCase().slice(0, 32); + +describe("getGuildApplicationCommand", () => { + it("happy path - fetches a freshly created guild application command by id", async () => { + if (!TEST_APPLICATION_ID || !TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID and DISCORD_TEST_GUILD_ID env vars are required for the getGuildApplicationCommand happy path", + ); + } + const name = cmdName("get"); + await runEffect( + Effect.gen(function* () { + const created = yield* createGuildApplicationCommand({ + application_id: TEST_APPLICATION_ID, + guild_id: TEST_GUILD_ID, + name, + description: "distilled test command", + }); + return yield* Effect.gen(function* () { + const fetched = yield* getGuildApplicationCommand({ + application_id: TEST_APPLICATION_ID, + guild_id: TEST_GUILD_ID, + command_id: created.id, + }); + expect(fetched.id).toBe(created.id); + expect(fetched.application_id).toBe(TEST_APPLICATION_ID); + expect(fetched.name).toBe(name); + expect(fetched.description).toBe("distilled test command"); + expect(typeof fetched.version).toBe("string"); + if (fetched.guild_id !== undefined) { + expect(fetched.guild_id).toBe(TEST_GUILD_ID); + } + }).pipe( + Effect.ensuring( + deleteGuildApplicationCommand({ + application_id: TEST_APPLICATION_ID, + guild_id: TEST_GUILD_ID, + command_id: created.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent command_id under the bot's application + guild", async () => { + if (!TEST_APPLICATION_ID || !TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID and DISCORD_TEST_GUILD_ID env vars are required for the NotFound test", + ); + } + await runEffect( + getGuildApplicationCommand({ + application_id: TEST_APPLICATION_ID, + guild_id: TEST_GUILD_ID, + command_id: NON_EXISTENT_COMMAND_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for application_id the bot does not own", async () => { + // Looking up a command under an application_id the bot's token does not + // own typically yields 403 Forbidden; may also surface as 404 NotFound + // when the route resolves the application before the permission check. + await runEffect( + getGuildApplicationCommand({ + application_id: NON_EXISTENT_APPLICATION_ID, + guild_id: NON_EXISTENT_GUILD_ID, + command_id: NON_EXISTENT_COMMAND_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getGuildApplicationCommandPermissions.test.ts b/packages/discord/test/getGuildApplicationCommandPermissions.test.ts new file mode 100644 index 000000000..8e3defa50 --- /dev/null +++ b/packages/discord/test/getGuildApplicationCommandPermissions.test.ts @@ -0,0 +1,109 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getGuildApplicationCommandPermissions } from "../src/operations/getGuildApplicationCommandPermissions.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +// The endpoint requires: +// - the bot's application_id (snowflake) — the bot's token must own it. +// - a guild the bot is a member of (DISCORD_TEST_GUILD_ID). +// - the command_id (snowflake) of a command in that guild that has at +// least one permission overwrite configured. Discord returns 404 for a +// command with no permissions configured, so the happy path requires +// the operator to supply such a command_id via +// DISCORD_TEST_GUILD_COMMAND_ID_WITH_PERMISSIONS. +// - the bearer token must carry the `applications.commands.permissions.update` +// scope (DISCORD_BEARER_TOKEN). Bot tokens cannot read these. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_COMMAND_ID_WITH_PERMISSIONS = + process.env.DISCORD_TEST_GUILD_COMMAND_ID_WITH_PERMISSIONS; + +// Snowflake-format identifiers that should not match real resources. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; +const NON_EXISTENT_GUILD_ID = "100000000000000001"; +const NON_EXISTENT_COMMAND_ID = "100000000000000002"; + +describe("getGuildApplicationCommandPermissions", () => { + it("happy path - fetches permissions for a guild application command", async () => { + if ( + !TEST_APPLICATION_ID || + !TEST_GUILD_ID || + !TEST_COMMAND_ID_WITH_PERMISSIONS + ) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID, DISCORD_TEST_GUILD_ID, and DISCORD_TEST_GUILD_COMMAND_ID_WITH_PERMISSIONS are required for the getGuildApplicationCommandPermissions happy path. Discord returns 404 for a command with no permissions configured; the bearer token must carry applications.commands.permissions.update scope.", + ); + } + const result = await runEffect( + getGuildApplicationCommandPermissions({ + application_id: TEST_APPLICATION_ID, + guild_id: TEST_GUILD_ID, + command_id: TEST_COMMAND_ID_WITH_PERMISSIONS, + }), + ); + expect(result.id).toBe(TEST_COMMAND_ID_WITH_PERMISSIONS); + expect(result.application_id).toBe(TEST_APPLICATION_ID); + expect(result.guild_id).toBe(TEST_GUILD_ID); + expect(Array.isArray(result.permissions)).toBe(true); + for (const overwrite of result.permissions) { + expect(typeof overwrite.id).toBe("string"); + expect(typeof overwrite.permission).toBe("boolean"); + } + }); + + it("error - NotFound for non-existent command_id under the bot's application + guild", async () => { + if (!TEST_APPLICATION_ID || !TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID and DISCORD_TEST_GUILD_ID env vars are required for the NotFound test", + ); + } + await runEffect( + getGuildApplicationCommandPermissions({ + application_id: TEST_APPLICATION_ID, + guild_id: TEST_GUILD_ID, + command_id: NON_EXISTENT_COMMAND_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden for application_id the bot does not own", async () => { + // Looking up command permissions under an application_id the bot's + // token does not own typically yields 403 Forbidden; may also surface + // as 404 NotFound when the route resolves the application before the + // permission check. + await runEffect( + getGuildApplicationCommandPermissions({ + application_id: NON_EXISTENT_APPLICATION_ID, + guild_id: NON_EXISTENT_GUILD_ID, + command_id: NON_EXISTENT_COMMAND_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getGuildBan.test.ts b/packages/discord/test/getGuildBan.test.ts new file mode 100644 index 000000000..811cf1a3d --- /dev/null +++ b/packages/discord/test/getGuildBan.test.ts @@ -0,0 +1,111 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { banUserFromGuild } from "../src/operations/banUserFromGuild.ts"; +import { getGuildBan } from "../src/operations/getGuildBan.ts"; +import { unbanUserFromGuild } from "../src/operations/unbanUserFromGuild.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +// The endpoint requires: +// - a guild the bot is in with BAN_MEMBERS permission. +// - the user_id (snowflake) of a user the bot is allowed to ban. The +// happy path bans the user, fetches the ban record, then unbans them. +// The operator must supply DISCORD_TEST_BANNABLE_USER_ID — a throwaway +// test account the bot can safely ban/unban. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_BANNABLE_USER_ID = process.env.DISCORD_TEST_BANNABLE_USER_ID; + +// Snowflake-format identifiers that should not match real resources. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_USER_ID = "100000000000000001"; + +describe("getGuildBan", () => { + it("happy path - bans a user, fetches the ban record, then unbans them", async () => { + if (!TEST_GUILD_ID || !TEST_BANNABLE_USER_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_BANNABLE_USER_ID env vars are required for the getGuildBan happy path. The user_id must be a throwaway test account the bot can safely ban/unban.", + ); + } + await runEffect( + Effect.gen(function* () { + yield* banUserFromGuild({ + guild_id: TEST_GUILD_ID, + user_id: TEST_BANNABLE_USER_ID, + delete_message_seconds: 0, + }); + return yield* Effect.gen(function* () { + const ban = yield* getGuildBan({ + guild_id: TEST_GUILD_ID, + user_id: TEST_BANNABLE_USER_ID, + }); + expect(ban.user.id).toBe(TEST_BANNABLE_USER_ID); + expect(typeof ban.user.username).toBe("string"); + expect(typeof ban.user.discriminator).toBe("string"); + // reason is nullable; assert null-or-string when present. + if (ban.reason !== null) { + expect(typeof ban.reason).toBe("string"); + } + }).pipe( + Effect.ensuring( + unbanUserFromGuild({ + guild_id: TEST_GUILD_ID, + user_id: TEST_BANNABLE_USER_ID, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for a user that is not banned in the guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + await runEffect( + getGuildBan({ + guild_id: TEST_GUILD_ID, + user_id: NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + getGuildBan({ + guild_id: NON_EXISTENT_GUILD_ID, + user_id: NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getGuildEmoji.test.ts b/packages/discord/test/getGuildEmoji.test.ts new file mode 100644 index 000000000..ec1d5f899 --- /dev/null +++ b/packages/discord/test/getGuildEmoji.test.ts @@ -0,0 +1,117 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildEmoji } from "../src/operations/createGuildEmoji.ts"; +import { deleteGuildEmoji } from "../src/operations/deleteGuildEmoji.ts"; +import { getGuildEmoji } from "../src/operations/getGuildEmoji.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Smallest valid 1x1 transparent PNG, encoded as a data URI. Discord accepts +// data URIs of the form "data:image/{png,jpeg,gif};base64,...". +const TINY_PNG_DATA_URI = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII="; + +// The endpoint requires: +// - a guild the bot is in with MANAGE_GUILD_EXPRESSIONS permission. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifiers that should not match real resources. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_EMOJI_ID = "100000000000000001"; + +// Discord requires emoji names to match ^[a-zA-Z0-9_]{2,32}$. +const emojiName = (suffix: string): string => { + const raw = `dt_${suffix}_${testRunId}`; + return raw.replace(/[^a-zA-Z0-9_]/g, "_").slice(0, 32); +}; + +describe("getGuildEmoji", () => { + it("happy path - fetches a freshly created guild emoji by id", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the getGuildEmoji happy path", + ); + } + const name = emojiName("get"); + await runEffect( + Effect.gen(function* () { + const created = yield* createGuildEmoji({ + guild_id: TEST_GUILD_ID, + name, + image: TINY_PNG_DATA_URI, + }); + return yield* Effect.gen(function* () { + const fetched = yield* getGuildEmoji({ + guild_id: TEST_GUILD_ID, + emoji_id: created.id, + }); + expect(fetched.id).toBe(created.id); + expect(fetched.name).toBe(name); + expect(Array.isArray(fetched.roles)).toBe(true); + expect(typeof fetched.require_colons).toBe("boolean"); + expect(typeof fetched.managed).toBe("boolean"); + expect(typeof fetched.animated).toBe("boolean"); + expect(typeof fetched.available).toBe("boolean"); + }).pipe( + Effect.ensuring( + deleteGuildEmoji({ + guild_id: TEST_GUILD_ID, + emoji_id: created.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent emoji_id under a real guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + await runEffect( + getGuildEmoji({ + guild_id: TEST_GUILD_ID, + emoji_id: NON_EXISTENT_EMOJI_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + getGuildEmoji({ + guild_id: NON_EXISTENT_GUILD_ID, + emoji_id: NON_EXISTENT_EMOJI_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getGuildJoinRequests.test.ts b/packages/discord/test/getGuildJoinRequests.test.ts new file mode 100644 index 000000000..f10a5292f --- /dev/null +++ b/packages/discord/test/getGuildJoinRequests.test.ts @@ -0,0 +1,90 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getGuildJoinRequests } from "../src/operations/getGuildJoinRequests.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +// The endpoint requires: +// - a guild the bot is in with MANAGE_GUILD permission. The guild must +// have membership screening or join requests enabled (Community guild +// with the join request review feature) for the response to contain +// records — but the endpoint returns a valid (possibly empty) payload +// even when the feature is disabled. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifier that should not match a real guild. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; + +describe("getGuildJoinRequests", () => { + it("happy path - lists join requests for the configured guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the getGuildJoinRequests happy path", + ); + } + const result = await runEffect( + getGuildJoinRequests({ + guild_id: TEST_GUILD_ID, + limit: 5, + }), + ); + expect(typeof result).toBe("object"); + expect(result).not.toBeNull(); + if (result.total !== undefined) { + expect(typeof result.total).toBe("number"); + expect(result.total).toBeGreaterThanOrEqual(0); + } + if (result.guild_join_requests !== undefined) { + expect(Array.isArray(result.guild_join_requests)).toBe(true); + for (const request of result.guild_join_requests) { + expect(typeof request.id).toBe("string"); + expect(typeof request.created_at).toBe("string"); + expect(request.guild_id).toBe(TEST_GUILD_ID); + expect(typeof request.user_id).toBe("string"); + } + } + }); + + it("error - NotFound for non-existent guild_id", async () => { + await runEffect( + getGuildJoinRequests({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unseen guilds, but may surface as + // Forbidden (50001 Missing Access) when the bot can't see the guild. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + getGuildJoinRequests({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getGuildMember.test.ts b/packages/discord/test/getGuildMember.test.ts new file mode 100644 index 000000000..4555e98ef --- /dev/null +++ b/packages/discord/test/getGuildMember.test.ts @@ -0,0 +1,92 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getGuildMember } from "../src/operations/getGuildMember.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +// The endpoint requires: +// - a guild the bot is in. +// - the user_id (snowflake) of a member of that guild. The bot is itself +// a member, so DISCORD_TEST_BOT_USER_ID is a reliable target. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_BOT_USER_ID = process.env.DISCORD_TEST_BOT_USER_ID; + +// Snowflake-format identifiers that should not match real resources. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_USER_ID = "100000000000000001"; + +describe("getGuildMember", () => { + it("happy path - fetches the bot's own member record in the configured guild", async () => { + if (!TEST_GUILD_ID || !TEST_BOT_USER_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_BOT_USER_ID env vars are required for the getGuildMember happy path", + ); + } + const result = await runEffect( + getGuildMember({ + guild_id: TEST_GUILD_ID, + user_id: TEST_BOT_USER_ID, + }), + ); + expect(result.user.id).toBe(TEST_BOT_USER_ID); + expect(typeof result.user.username).toBe("string"); + expect(typeof result.joined_at).toBe("string"); + expect(Array.isArray(result.roles)).toBe(true); + expect(typeof result.flags).toBe("number"); + expect(typeof result.pending).toBe("boolean"); + expect(typeof result.mute).toBe("boolean"); + expect(typeof result.deaf).toBe("boolean"); + }); + + it("error - NotFound for a user that is not a member of the guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + await runEffect( + getGuildMember({ + guild_id: TEST_GUILD_ID, + user_id: NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + getGuildMember({ + guild_id: NON_EXISTENT_GUILD_ID, + user_id: NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getGuildNewMemberWelcome.test.ts b/packages/discord/test/getGuildNewMemberWelcome.test.ts new file mode 100644 index 000000000..f20fc5ac4 --- /dev/null +++ b/packages/discord/test/getGuildNewMemberWelcome.test.ts @@ -0,0 +1,88 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getGuildNewMemberWelcome } from "../src/operations/getGuildNewMemberWelcome.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +// The endpoint requires: +// - a Community guild the bot is in with MANAGE_GUILD permission and the +// new-member-welcome experience configured. Operators must supply +// DISCORD_TEST_GUILD_ID_WITH_NEW_MEMBER_WELCOME pointing to such a guild; +// plain test guilds without the feature return 404. +const TEST_GUILD_ID_WITH_NEW_MEMBER_WELCOME = + process.env.DISCORD_TEST_GUILD_ID_WITH_NEW_MEMBER_WELCOME; + +// Snowflake-format identifier that should not match a real guild. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; + +describe("getGuildNewMemberWelcome", () => { + it("happy path - fetches the new-member welcome configuration for the guild", async () => { + if (!TEST_GUILD_ID_WITH_NEW_MEMBER_WELCOME) { + throw new Error( + "DISCORD_TEST_GUILD_ID_WITH_NEW_MEMBER_WELCOME env var is required for the getGuildNewMemberWelcome happy path. The guild must be a Community guild with the new-member-welcome experience configured; otherwise Discord returns 404.", + ); + } + const result = await runEffect( + getGuildNewMemberWelcome({ + guild_id: TEST_GUILD_ID_WITH_NEW_MEMBER_WELCOME, + }), + ); + expect(result.guild_id).toBe(TEST_GUILD_ID_WITH_NEW_MEMBER_WELCOME); + expect(typeof result.enabled).toBe("boolean"); + expect(Array.isArray(result.new_member_actions)).toBe(true); + expect(Array.isArray(result.resource_channels)).toBe(true); + for (const action of result.new_member_actions) { + expect(typeof action.channel_id).toBe("string"); + expect(typeof action.title).toBe("string"); + expect(typeof action.description).toBe("string"); + } + for (const resource of result.resource_channels) { + expect(typeof resource.channel_id).toBe("string"); + expect(typeof resource.title).toBe("string"); + expect(typeof resource.description).toBe("string"); + } + }); + + it("error - NotFound for non-existent guild_id", async () => { + await runEffect( + getGuildNewMemberWelcome({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unseen guilds, but may surface as + // Forbidden (50001 Missing Access) when the bot can't see the guild. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + getGuildNewMemberWelcome({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getGuildPreview.test.ts b/packages/discord/test/getGuildPreview.test.ts new file mode 100644 index 000000000..95cb0c4d6 --- /dev/null +++ b/packages/discord/test/getGuildPreview.test.ts @@ -0,0 +1,75 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getGuildPreview } from "../src/operations/getGuildPreview.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +// The endpoint requires: +// - the guild_id (snowflake) of a guild that is either discoverable +// (DISCOVERABLE feature) or one the bot is a member of. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifier that should not match a real guild. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; + +describe("getGuildPreview", () => { + it("happy path - fetches the preview for the configured guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the getGuildPreview happy path", + ); + } + const result = await runEffect(getGuildPreview({ guild_id: TEST_GUILD_ID })); + expect(result.id).toBe(TEST_GUILD_ID); + expect(typeof result.name).toBe("string"); + expect(typeof result.approximate_member_count).toBe("number"); + expect(typeof result.approximate_presence_count).toBe("number"); + expect(Array.isArray(result.features)).toBe(true); + expect(Array.isArray(result.emojis)).toBe(true); + expect(Array.isArray(result.stickers)).toBe(true); + }); + + it("error - NotFound for non-existent guild_id", async () => { + await runEffect( + getGuildPreview({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unseen guilds, but may surface as + // Forbidden (50001 Missing Access) when the bot can't see the guild + // and the guild is not discoverable. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot cannot see and is not discoverable", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see and + // which is not DISCOVERABLE typically yields Forbidden, or NotFound if + // the route resolves the guild before the permission check. + await runEffect( + getGuildPreview({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getGuildRole.test.ts b/packages/discord/test/getGuildRole.test.ts new file mode 100644 index 000000000..c02d75544 --- /dev/null +++ b/packages/discord/test/getGuildRole.test.ts @@ -0,0 +1,113 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildRole } from "../src/operations/createGuildRole.ts"; +import { deleteGuildRole } from "../src/operations/deleteGuildRole.ts"; +import { getGuildRole } from "../src/operations/getGuildRole.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint requires: +// - a guild the bot is in with MANAGE_ROLES permission. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifiers that should not match real resources. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_ROLE_ID = "100000000000000001"; + +const roleName = (suffix: string): string => + `distilled-${suffix}-${testRunId}`.slice(0, 100); + +describe("getGuildRole", () => { + it("happy path - fetches a freshly created guild role by id", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the getGuildRole happy path", + ); + } + const name = roleName("get"); + await runEffect( + Effect.gen(function* () { + const created = yield* createGuildRole({ + guild_id: TEST_GUILD_ID, + name, + mentionable: false, + hoist: false, + }); + return yield* Effect.gen(function* () { + const fetched = yield* getGuildRole({ + guild_id: TEST_GUILD_ID, + role_id: created.id, + }); + expect(fetched.id).toBe(created.id); + expect(fetched.name).toBe(name); + expect(typeof fetched.permissions).toBe("string"); + expect(typeof fetched.position).toBe("number"); + expect(typeof fetched.color).toBe("number"); + expect(typeof fetched.hoist).toBe("boolean"); + expect(typeof fetched.managed).toBe("boolean"); + expect(typeof fetched.mentionable).toBe("boolean"); + expect(typeof fetched.flags).toBe("number"); + expect(typeof fetched.colors.primary_color).toBe("number"); + }).pipe( + Effect.ensuring( + deleteGuildRole({ + guild_id: TEST_GUILD_ID, + role_id: created.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }); + + it("error - NotFound for non-existent role_id under a real guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + await runEffect( + getGuildRole({ + guild_id: TEST_GUILD_ID, + role_id: NON_EXISTENT_ROLE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + getGuildRole({ + guild_id: NON_EXISTENT_GUILD_ID, + role_id: NON_EXISTENT_ROLE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getGuildScheduledEvent.test.ts b/packages/discord/test/getGuildScheduledEvent.test.ts new file mode 100644 index 000000000..9e9b0934e --- /dev/null +++ b/packages/discord/test/getGuildScheduledEvent.test.ts @@ -0,0 +1,107 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getGuildScheduledEvent } from "../src/operations/getGuildScheduledEvent.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +// The endpoint requires: +// - a guild the bot is in. +// - the snowflake of an existing scheduled event in that guild. The +// SDK's createGuildScheduledEvent does not currently surface its body +// schema (codegen gap), so the happy path requires the operator to +// supply an existing event_id via DISCORD_TEST_GUILD_SCHEDULED_EVENT_ID. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_GUILD_SCHEDULED_EVENT_ID = + process.env.DISCORD_TEST_GUILD_SCHEDULED_EVENT_ID; + +// Snowflake-format identifiers that should not match real resources. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_EVENT_ID = "100000000000000001"; + +describe("getGuildScheduledEvent", () => { + it("happy path - fetches a guild scheduled event by id with user counts", async () => { + if (!TEST_GUILD_ID || !TEST_GUILD_SCHEDULED_EVENT_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_GUILD_SCHEDULED_EVENT_ID env vars are required for the getGuildScheduledEvent happy path", + ); + } + const result = await runEffect( + getGuildScheduledEvent({ + guild_id: TEST_GUILD_ID, + guild_scheduled_event_id: TEST_GUILD_SCHEDULED_EVENT_ID, + with_user_count: true, + }), + ); + // The output is typed as an opaque value because the spec does not + // describe the response body. Cast for assertions. + const event = result as { + id?: string; + guild_id?: string; + name?: string; + status?: number; + }; + expect(typeof event).toBe("object"); + expect(event).not.toBeNull(); + if (event.id !== undefined) { + expect(event.id).toBe(TEST_GUILD_SCHEDULED_EVENT_ID); + } + if (event.guild_id !== undefined) { + expect(event.guild_id).toBe(TEST_GUILD_ID); + } + if (event.name !== undefined) { + expect(typeof event.name).toBe("string"); + } + }); + + it("error - NotFound for non-existent event_id under a real guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + await runEffect( + getGuildScheduledEvent({ + guild_id: TEST_GUILD_ID, + guild_scheduled_event_id: NON_EXISTENT_EVENT_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + getGuildScheduledEvent({ + guild_id: NON_EXISTENT_GUILD_ID, + guild_scheduled_event_id: NON_EXISTENT_EVENT_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getGuildSoundboardSound.test.ts b/packages/discord/test/getGuildSoundboardSound.test.ts new file mode 100644 index 000000000..d19559e28 --- /dev/null +++ b/packages/discord/test/getGuildSoundboardSound.test.ts @@ -0,0 +1,94 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getGuildSoundboardSound } from "../src/operations/getGuildSoundboardSound.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +// The endpoint requires: +// - a guild the bot is in. +// - the sound_id (snowflake) of an existing soundboard sound in that +// guild. Creating soundboard sounds requires uploading mp3/ogg audio, +// which is not practical in-test, so the happy path requires the +// operator to supply DISCORD_TEST_GUILD_SOUNDBOARD_SOUND_ID. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_GUILD_SOUNDBOARD_SOUND_ID = + process.env.DISCORD_TEST_GUILD_SOUNDBOARD_SOUND_ID; + +// Snowflake-format identifiers that should not match real resources. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_SOUND_ID = "100000000000000001"; + +describe("getGuildSoundboardSound", () => { + it("happy path - fetches a guild soundboard sound by id", async () => { + if (!TEST_GUILD_ID || !TEST_GUILD_SOUNDBOARD_SOUND_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_GUILD_SOUNDBOARD_SOUND_ID env vars are required for the getGuildSoundboardSound happy path. Soundboard sounds require uploading mp3/ogg audio so cannot be created in-test.", + ); + } + const result = await runEffect( + getGuildSoundboardSound({ + guild_id: TEST_GUILD_ID, + sound_id: TEST_GUILD_SOUNDBOARD_SOUND_ID, + }), + ); + expect(result.sound_id).toBe(TEST_GUILD_SOUNDBOARD_SOUND_ID); + expect(typeof result.name).toBe("string"); + expect(typeof result.volume).toBe("number"); + expect(typeof result.available).toBe("boolean"); + if (result.guild_id !== undefined) { + expect(result.guild_id).toBe(TEST_GUILD_ID); + } + }); + + it("error - NotFound for non-existent sound_id under a real guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + await runEffect( + getGuildSoundboardSound({ + guild_id: TEST_GUILD_ID, + sound_id: NON_EXISTENT_SOUND_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + getGuildSoundboardSound({ + guild_id: NON_EXISTENT_GUILD_ID, + sound_id: NON_EXISTENT_SOUND_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getGuildSticker.test.ts b/packages/discord/test/getGuildSticker.test.ts new file mode 100644 index 000000000..79682c2fa --- /dev/null +++ b/packages/discord/test/getGuildSticker.test.ts @@ -0,0 +1,94 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getGuildSticker } from "../src/operations/getGuildSticker.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +// The endpoint requires: +// - a guild the bot is in. +// - the sticker_id (snowflake) of an existing sticker in that guild. +// Creating stickers requires uploading PNG/APNG/Lottie/GIF files via +// multipart, which is not practical in-test, so the happy path requires +// the operator to supply DISCORD_TEST_GUILD_STICKER_ID. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_GUILD_STICKER_ID = process.env.DISCORD_TEST_GUILD_STICKER_ID; + +// Snowflake-format identifiers that should not match real resources. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_STICKER_ID = "100000000000000001"; + +describe("getGuildSticker", () => { + it("happy path - fetches a guild sticker by id", async () => { + if (!TEST_GUILD_ID || !TEST_GUILD_STICKER_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_GUILD_STICKER_ID env vars are required for the getGuildSticker happy path. Stickers require multipart uploads so cannot be created in-test.", + ); + } + const result = await runEffect( + getGuildSticker({ + guild_id: TEST_GUILD_ID, + sticker_id: TEST_GUILD_STICKER_ID, + }), + ); + expect(result.id).toBe(TEST_GUILD_STICKER_ID); + expect(result.guild_id).toBe(TEST_GUILD_ID); + expect(typeof result.name).toBe("string"); + expect(typeof result.tags).toBe("string"); + expect(typeof result.available).toBe("boolean"); + if (result.description !== null) { + expect(typeof result.description).toBe("string"); + } + }); + + it("error - NotFound for non-existent sticker_id under a real guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + await runEffect( + getGuildSticker({ + guild_id: TEST_GUILD_ID, + sticker_id: NON_EXISTENT_STICKER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + getGuildSticker({ + guild_id: NON_EXISTENT_GUILD_ID, + sticker_id: NON_EXISTENT_STICKER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getGuildTemplate.test.ts b/packages/discord/test/getGuildTemplate.test.ts new file mode 100644 index 000000000..dd0df026c --- /dev/null +++ b/packages/discord/test/getGuildTemplate.test.ts @@ -0,0 +1,119 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildTemplate } from "../src/operations/createGuildTemplate.ts"; +import { deleteGuildTemplate } from "../src/operations/deleteGuildTemplate.ts"; +import { getGuildTemplate } from "../src/operations/getGuildTemplate.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint is GET /guilds/templates/{code} — a public-ish lookup by +// template share code. The happy path either: +// - uses an operator-supplied DISCORD_TEST_GUILD_TEMPLATE_CODE, or +// - if a DISCORD_TEST_GUILD_ID is provided, creates a template on that +// guild, fetches it, and ensures cleanup. (Discord allows only one +// template per guild, so creation may BadRequest if one already exists; +// in that case the test is skipped via the env-var fallback.) +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_GUILD_TEMPLATE_CODE = process.env.DISCORD_TEST_GUILD_TEMPLATE_CODE; + +// Template codes are short opaque strings (not snowflakes). A made-up code +// that does not match any real template should yield NotFound. +const NON_EXISTENT_TEMPLATE_CODE = `distilled-no-such-${testRunId}`; + +describe("getGuildTemplate", () => { + it("happy path - fetches a guild template by code", async () => { + if (TEST_GUILD_TEMPLATE_CODE) { + const result = await runEffect( + getGuildTemplate({ code: TEST_GUILD_TEMPLATE_CODE }), + ); + expect(result.code).toBe(TEST_GUILD_TEMPLATE_CODE); + expect(typeof result.name).toBe("string"); + expect(typeof result.usage_count).toBe("number"); + expect(typeof result.creator_id).toBe("string"); + expect(typeof result.source_guild_id).toBe("string"); + expect(typeof result.serialized_source_guild.name).toBe("string"); + expect(Array.isArray(result.serialized_source_guild.roles)).toBe(true); + expect(Array.isArray(result.serialized_source_guild.channels)).toBe(true); + return; + } + if (!TEST_GUILD_ID) { + throw new Error( + "Either DISCORD_TEST_GUILD_TEMPLATE_CODE or DISCORD_TEST_GUILD_ID must be set for the getGuildTemplate happy path.", + ); + } + const created = await runEffect( + createGuildTemplate({ + guild_id: TEST_GUILD_ID, + name: `distilled-discord-template-${testRunId}`, + description: `distilled test template ${testRunId}`, + }), + ); + try { + const result = await runEffect(getGuildTemplate({ code: created.code })); + expect(result.code).toBe(created.code); + expect(result.source_guild_id).toBe(TEST_GUILD_ID); + expect(typeof result.name).toBe("string"); + expect(typeof result.usage_count).toBe("number"); + expect(typeof result.creator_id).toBe("string"); + expect(typeof result.serialized_source_guild.name).toBe("string"); + expect(Array.isArray(result.serialized_source_guild.roles)).toBe(true); + expect(Array.isArray(result.serialized_source_guild.channels)).toBe(true); + } finally { + await runEffect( + deleteGuildTemplate({ + guild_id: TEST_GUILD_ID, + code: created.code, + }).pipe(Effect.ignore), + ); + } + }); + + it("error - NotFound for a non-existent template code", async () => { + await runEffect( + getGuildTemplate({ code: NON_EXISTENT_TEMPLATE_CODE }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound (10057 Unknown Guild Template) for codes + // that do not match any template; some malformed codes may also + // surface as BadRequest, and revoked/banned codes as Forbidden. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound or Forbidden for an obviously invalid code shape", async () => { + // A clearly invalid template code shape — Discord's gateway typically + // resolves this as NotFound, but a stricter validator may yield + // BadRequest, and a banned code may yield Forbidden. + await runEffect( + getGuildTemplate({ code: `${NON_EXISTENT_TEMPLATE_CODE}-x!` }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getGuildVanityUrl.test.ts b/packages/discord/test/getGuildVanityUrl.test.ts new file mode 100644 index 000000000..d8cc0c0d3 --- /dev/null +++ b/packages/discord/test/getGuildVanityUrl.test.ts @@ -0,0 +1,84 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getGuildVanityUrl } from "../src/operations/getGuildVanityUrl.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/vanity-url returns the vanity invite URL config for +// the guild. Requires MANAGE_GUILD on the bot's member of that guild. +// The guild itself does not need to have a vanity code set — Discord returns +// `{ code: null, uses: 0 }` in that case (still 200). +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// A snowflake unlikely to correspond to any real guild — Discord typically +// responds with 404 (Unknown Guild → NotFound) or 403 (Missing Access → +// Forbidden) depending on how it interprets the lookup. +const NON_EXISTENT_GUILD_ID = `100000000000000000-${testRunId}`.slice(0, 18); + +describe("getGuildVanityUrl", () => { + it("happy path - fetches the vanity url config for a guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID must be set for the getGuildVanityUrl happy path. " + + "The bot must have MANAGE_GUILD on this guild.", + ); + } + const result = await runEffect( + getGuildVanityUrl({ guild_id: TEST_GUILD_ID }), + ); + // `code` is nullable: null when the guild has no vanity URL configured, + // or a string when it does. + expect(result.code === null || typeof result.code === "string").toBe(true); + expect(typeof result.uses).toBe("number"); + expect(result.uses).toBeGreaterThanOrEqual(0); + }); + + it("error - NotFound for a non-existent guild id", async () => { + await runEffect( + getGuildVanityUrl({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may surface a missing guild as NotFound (10004 Unknown + // Guild), or as Forbidden (Missing Access) when the bot is not in + // the guild. Some malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot cannot access", async () => { + // Use a snowflake-shaped id that is extremely unlikely to belong to a + // guild the bot can read. Discord prefers Forbidden when the resource + // exists-but-inaccessible, and NotFound when it does not exist. + await runEffect( + getGuildVanityUrl({ guild_id: "100000000000000001" }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getGuildWebhooks.test.ts b/packages/discord/test/getGuildWebhooks.test.ts new file mode 100644 index 000000000..84baeec5b --- /dev/null +++ b/packages/discord/test/getGuildWebhooks.test.ts @@ -0,0 +1,82 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getGuildWebhooks } from "../src/operations/getGuildWebhooks.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/webhooks lists every webhook across all channels in +// the guild. Requires MANAGE_WEBHOOKS on the bot's guild member. The list +// may legitimately be empty. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids the bot cannot read. The first should produce +// NotFound (guild does not exist) and the second a Forbidden (or NotFound +// depending on Discord's resolution order) for an inaccessible guild. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("getGuildWebhooks", () => { + it("happy path - lists webhooks for the test guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID must be set for the getGuildWebhooks happy path. " + + "The bot must have MANAGE_WEBHOOKS on this guild.", + ); + } + const result = await runEffect( + getGuildWebhooks({ guild_id: TEST_GUILD_ID }), + ); + expect(Array.isArray(result)).toBe(true); + // Each entry is an opaque webhook object; we don't assert its shape + // because the output schema is `Array`. + for (const entry of result) { + expect(entry).toBeDefined(); + } + }); + + it("error - NotFound for a non-existent guild id", async () => { + await runEffect( + getGuildWebhooks({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may surface a missing guild as NotFound (10004), or as + // Forbidden (Missing Access) when the bot is not in the guild. + // Some malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot cannot access", async () => { + await runEffect( + getGuildWebhooks({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getGuildWelcomeScreen.test.ts b/packages/discord/test/getGuildWelcomeScreen.test.ts new file mode 100644 index 000000000..c9121483e --- /dev/null +++ b/packages/discord/test/getGuildWelcomeScreen.test.ts @@ -0,0 +1,91 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getGuildWelcomeScreen } from "../src/operations/getGuildWelcomeScreen.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/welcome-screen returns the welcome screen for a +// Community-enabled guild. If the guild has no welcome screen configured +// Discord responds 404 (NotFound 10069 — Welcome screens only exist for +// Community guilds with one set up). The test guild therefore must be a +// Community guild that has a welcome screen configured. +const TEST_GUILD_ID = + process.env.DISCORD_TEST_GUILD_WITH_WELCOME_SCREEN_ID ?? + process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids that should not resolve to any guild the bot can +// read. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("getGuildWelcomeScreen", () => { + it("happy path - fetches the welcome screen of the test guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_WITH_WELCOME_SCREEN_ID (or DISCORD_TEST_GUILD_ID) must be set " + + "for the getGuildWelcomeScreen happy path. The guild must be Community-enabled " + + "with a welcome screen configured.", + ); + } + const result = await runEffect( + getGuildWelcomeScreen({ guild_id: TEST_GUILD_ID }), + ); + expect( + result.description === null || typeof result.description === "string", + ).toBe(true); + expect(Array.isArray(result.welcome_channels)).toBe(true); + for (const wc of result.welcome_channels) { + expect(typeof wc.channel_id).toBe("string"); + expect(typeof wc.description).toBe("string"); + expect(wc.emoji_name === null || typeof wc.emoji_name === "string").toBe( + true, + ); + } + }); + + it("error - NotFound for a non-existent guild id", async () => { + await runEffect( + getGuildWelcomeScreen({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may surface a missing guild as NotFound (10004), or as + // Forbidden (Missing Access) when the bot is not in the guild. + // Some malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot cannot access", async () => { + await runEffect( + getGuildWelcomeScreen({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getGuildWidget.test.ts b/packages/discord/test/getGuildWidget.test.ts new file mode 100644 index 000000000..9cca19ef9 --- /dev/null +++ b/packages/discord/test/getGuildWidget.test.ts @@ -0,0 +1,99 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getGuildWidget } from "../src/operations/getGuildWidget.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/widget.json is the *public* widget JSON. The guild +// must have the widget feature enabled — otherwise Discord responds 403 +// (Widget Disabled → Forbidden). The bot does not need to be in the guild. +const TEST_GUILD_ID = + process.env.DISCORD_TEST_GUILD_WITH_WIDGET_ID ?? + process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real guild. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("getGuildWidget", () => { + it("happy path - fetches the public widget for the test guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_WITH_WIDGET_ID (or DISCORD_TEST_GUILD_ID) must be set " + + "for the getGuildWidget happy path. The guild must have the widget enabled.", + ); + } + const result = await runEffect(getGuildWidget({ guild_id: TEST_GUILD_ID })); + expect(typeof result.id).toBe("string"); + expect(typeof result.name).toBe("string"); + expect( + result.instant_invite === null || + typeof result.instant_invite === "string", + ).toBe(true); + expect(Array.isArray(result.channels)).toBe(true); + for (const ch of result.channels) { + expect(typeof ch.id).toBe("string"); + expect(typeof ch.name).toBe("string"); + expect(typeof ch.position).toBe("number"); + } + expect(Array.isArray(result.members)).toBe(true); + for (const m of result.members) { + expect(typeof m.id).toBe("string"); + expect(typeof m.username).toBe("string"); + expect(typeof m.status).toBe("string"); + expect(typeof m.avatar_url).toBe("string"); + } + expect(typeof result.presence_count).toBe("number"); + expect(result.presence_count).toBeGreaterThanOrEqual(0); + }); + + it("error - NotFound for a non-existent guild id", async () => { + await runEffect( + getGuildWidget({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (10004 — guild does not exist), or + // Forbidden (Widget Disabled) for guilds that exist but have not + // enabled their widget. Some malformed snowflakes may surface as + // BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when the widget is disabled on a guild", async () => { + // For a snowflake-shaped id pointing at a guild whose widget is not + // enabled, Discord responds with 403 Widget Disabled (→ Forbidden). + // For a non-resolving id it returns 404 (→ NotFound). + await runEffect( + getGuildWidget({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getGuildWidgetPng.test.ts b/packages/discord/test/getGuildWidgetPng.test.ts new file mode 100644 index 000000000..6df6d8e2c --- /dev/null +++ b/packages/discord/test/getGuildWidgetPng.test.ts @@ -0,0 +1,92 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getGuildWidgetPng } from "../src/operations/getGuildWidgetPng.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/widget.png returns a PNG image of the guild widget. +// Output schema is Void — the SDK simply resolves on a 2xx response. The +// guild must have the widget feature enabled, otherwise Discord responds 403 +// (Widget Disabled → Forbidden). The bot does not need to be in the guild. +const TEST_GUILD_ID = + process.env.DISCORD_TEST_GUILD_WITH_WIDGET_ID ?? + process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real guild. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("getGuildWidgetPng", () => { + it("happy path - fetches the widget PNG for the test guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_WITH_WIDGET_ID (or DISCORD_TEST_GUILD_ID) must be set " + + "for the getGuildWidgetPng happy path. The guild must have the widget enabled.", + ); + } + const result = await runEffect( + getGuildWidgetPng({ guild_id: TEST_GUILD_ID }), + ); + // Output schema is Void — successful resolution is the assertion. + expect(result).toBeUndefined(); + }); + + it("happy path - accepts a `style` query parameter", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_WITH_WIDGET_ID (or DISCORD_TEST_GUILD_ID) must be set " + + "for the getGuildWidgetPng happy path. The guild must have the widget enabled.", + ); + } + const result = await runEffect( + getGuildWidgetPng({ guild_id: TEST_GUILD_ID, style: "banner1" }), + ); + expect(result).toBeUndefined(); + }); + + it("error - NotFound for a non-existent guild id", async () => { + await runEffect( + getGuildWidgetPng({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (10004 — guild does not exist), or + // Forbidden (Widget Disabled) for guilds whose widget is not + // enabled. Some malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when the widget is disabled on a guild", async () => { + await runEffect( + getGuildWidgetPng({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getGuildWidgetSettings.test.ts b/packages/discord/test/getGuildWidgetSettings.test.ts new file mode 100644 index 000000000..f0f6d7925 --- /dev/null +++ b/packages/discord/test/getGuildWidgetSettings.test.ts @@ -0,0 +1,79 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getGuildWidgetSettings } from "../src/operations/getGuildWidgetSettings.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/widget returns the *settings* of the widget +// (enabled flag and target channel id). Requires MANAGE_GUILD on the bot's +// member of the guild. Unlike the public widget.json endpoint, this works +// regardless of whether the widget is currently enabled. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to a guild the bot can access. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("getGuildWidgetSettings", () => { + it("happy path - fetches widget settings for the test guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID must be set for the getGuildWidgetSettings happy path. " + + "The bot must have MANAGE_GUILD on this guild.", + ); + } + const result = await runEffect( + getGuildWidgetSettings({ guild_id: TEST_GUILD_ID }), + ); + expect(typeof result.enabled).toBe("boolean"); + // channel_id is `Schema.Unknown` — Discord returns either a snowflake + // string or null. We assert only that the property is present. + expect("channel_id" in result).toBe(true); + }); + + it("error - NotFound for a non-existent guild id", async () => { + await runEffect( + getGuildWidgetSettings({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may surface a missing guild as NotFound (10004), or as + // Forbidden (Missing Access) when the bot is not in the guild. + // Some malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot cannot access", async () => { + await runEffect( + getGuildWidgetSettings({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getGuildsOnboarding.test.ts b/packages/discord/test/getGuildsOnboarding.test.ts new file mode 100644 index 000000000..d4377a1b3 --- /dev/null +++ b/packages/discord/test/getGuildsOnboarding.test.ts @@ -0,0 +1,82 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getGuildsOnboarding } from "../src/operations/getGuildsOnboarding.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +// The endpoint requires: +// - a guild the bot is in. Discord returns the onboarding object even +// when onboarding is disabled (with `enabled: false` and possibly empty +// prompts/default_channel_ids), so any test guild works. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifier that should not match a real guild. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; + +describe("getGuildsOnboarding", () => { + it("happy path - fetches the onboarding configuration for the configured guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the getGuildsOnboarding happy path", + ); + } + const result = await runEffect( + getGuildsOnboarding({ guild_id: TEST_GUILD_ID }), + ); + expect(result.guild_id).toBe(TEST_GUILD_ID); + expect(typeof result.enabled).toBe("boolean"); + expect(Array.isArray(result.prompts)).toBe(true); + expect(Array.isArray(result.default_channel_ids)).toBe(true); + for (const prompt of result.prompts) { + expect(typeof prompt.id).toBe("string"); + expect(typeof prompt.title).toBe("string"); + expect(typeof prompt.single_select).toBe("boolean"); + expect(typeof prompt.required).toBe("boolean"); + expect(typeof prompt.in_onboarding).toBe("boolean"); + expect(Array.isArray(prompt.options)).toBe(true); + } + }); + + it("error - NotFound for non-existent guild_id", async () => { + await runEffect( + getGuildsOnboarding({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for unseen guilds, but may surface as + // Forbidden (50001 Missing Access) when the bot can't see the guild. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // Calling against a snowflake-shaped guild_id the bot does not see + // typically yields Forbidden (50001 Missing Access), or NotFound if the + // route resolves the guild before the permission check. + await runEffect( + getGuildsOnboarding({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound"]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getInviteTargetUsers.test.ts b/packages/discord/test/getInviteTargetUsers.test.ts new file mode 100644 index 000000000..dac97b53a --- /dev/null +++ b/packages/discord/test/getInviteTargetUsers.test.ts @@ -0,0 +1,82 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getInviteTargetUsers } from "../src/operations/getInviteTargetUsers.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /invites/{code}/target-users — returns the target users associated +// with an invite (used for "join activity" / stream invites). Output schema +// is Void in the SDK, so a successful call resolves with `undefined`. +// +// This endpoint is restrictive: most regular invites return 404. Operators +// must supply DISCORD_TEST_INVITE_TARGET_USERS_CODE pointing at an invite +// with target users (typically a stream / activity invite created in the +// test guild). +const TEST_INVITE_CODE = process.env.DISCORD_TEST_INVITE_TARGET_USERS_CODE; + +// Invite codes are short opaque strings, not snowflakes. A made-up code +// that does not match any real invite should yield NotFound. +const NON_EXISTENT_INVITE_CODE = `distilled-no-such-${testRunId}`; + +describe("getInviteTargetUsers", () => { + it("happy path - fetches target users for an invite", async () => { + if (!TEST_INVITE_CODE) { + throw new Error( + "DISCORD_TEST_INVITE_TARGET_USERS_CODE must be set for the getInviteTargetUsers " + + "happy path. The invite must be one with target users (e.g. a stream / " + + "activity invite).", + ); + } + const result = await runEffect( + getInviteTargetUsers({ code: TEST_INVITE_CODE }), + ); + // Output schema is Void — successful resolution is the assertion. + expect(result).toBeUndefined(); + }); + + it("error - NotFound for a non-existent invite code", async () => { + await runEffect( + getInviteTargetUsers({ code: NON_EXISTENT_INVITE_CODE }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord typically returns NotFound (10006 — invalid invite) for + // a code that does not match any invite. Some malformed codes may + // surface as BadRequest, and revoked codes as Forbidden. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound or Forbidden for an obviously invalid code shape", async () => { + await runEffect( + getInviteTargetUsers({ code: `${NON_EXISTENT_INVITE_CODE}-x!` }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getInviteTargetUsersJobStatus.test.ts b/packages/discord/test/getInviteTargetUsersJobStatus.test.ts new file mode 100644 index 000000000..b6f1a58b3 --- /dev/null +++ b/packages/discord/test/getInviteTargetUsersJobStatus.test.ts @@ -0,0 +1,92 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getInviteTargetUsersJobStatus } from "../src/operations/getInviteTargetUsersJobStatus.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /invites/{code}/target-users/job-status — returns the status of the +// background job that resolves target users for an invite. Operators must +// supply DISCORD_TEST_INVITE_TARGET_USERS_CODE (an invite with target users) +// for the happy path; most regular invites return 404 here. +const TEST_INVITE_CODE = process.env.DISCORD_TEST_INVITE_TARGET_USERS_CODE; + +// Invite codes are short opaque strings, not snowflakes. A made-up code +// that does not match any real invite should yield NotFound. +const NON_EXISTENT_INVITE_CODE = `distilled-no-such-${testRunId}`; + +describe("getInviteTargetUsersJobStatus", () => { + it("happy path - fetches the target-users job status for an invite", async () => { + if (!TEST_INVITE_CODE) { + throw new Error( + "DISCORD_TEST_INVITE_TARGET_USERS_CODE must be set for the " + + "getInviteTargetUsersJobStatus happy path. The invite must be one " + + "with target users (e.g. a stream / activity invite).", + ); + } + const result = await runEffect( + getInviteTargetUsersJobStatus({ code: TEST_INVITE_CODE }), + ); + expect("status" in result).toBe(true); + expect(typeof result.total_users).toBe("number"); + expect(result.total_users).toBeGreaterThanOrEqual(0); + expect(typeof result.processed_users).toBe("number"); + expect(result.processed_users).toBeGreaterThanOrEqual(0); + expect( + result.created_at === null || typeof result.created_at === "string", + ).toBe(true); + expect( + result.completed_at === null || typeof result.completed_at === "string", + ).toBe(true); + expect( + result.error_message === null || typeof result.error_message === "string", + ).toBe(true); + }); + + it("error - NotFound for a non-existent invite code", async () => { + await runEffect( + getInviteTargetUsersJobStatus({ code: NON_EXISTENT_INVITE_CODE }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord typically returns NotFound (10006 — invalid invite) for + // a code that does not match any invite. Some malformed codes may + // surface as BadRequest, and revoked codes as Forbidden. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound or Forbidden for an obviously invalid code shape", async () => { + await runEffect( + getInviteTargetUsersJobStatus({ + code: `${NON_EXISTENT_INVITE_CODE}-x!`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getLobby.test.ts b/packages/discord/test/getLobby.test.ts new file mode 100644 index 000000000..8728e3734 --- /dev/null +++ b/packages/discord/test/getLobby.test.ts @@ -0,0 +1,92 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createLobby } from "../src/operations/createLobby.ts"; +import { getLobby } from "../src/operations/getLobby.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /lobbies/{lobby_id} fetches a lobby (Social SDK / Activities API). +// The bot's application must have the LOBBIES_WRITE scope. There is no +// deleteLobby endpoint — lobbies are reaped by `idle_timeout_seconds` after +// the last member leaves, so the test creates a short-idle lobby and lets +// it self-expire. +const TEST_LOBBY_ID = process.env.DISCORD_TEST_LOBBY_ID; + +// Snowflake-shaped ids unlikely to resolve to any real lobby. +const NON_EXISTENT_LOBBY_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_LOBBY_ID = "100000000000000001"; + +describe("getLobby", () => { + it("happy path - fetches a lobby", async () => { + let lobbyId = TEST_LOBBY_ID; + if (!lobbyId) { + // Create a short-idle lobby. The application must have LOBBIES_WRITE. + const created = await runEffect( + createLobby({ + idle_timeout_seconds: 5, + metadata: { distilled_test: testRunId }, + }), + ); + lobbyId = created.id; + } + const result = await runEffect(getLobby({ lobby_id: lobbyId })); + expect(typeof result.id).toBe("string"); + expect(result.id).toBe(lobbyId); + expect(typeof result.application_id).toBe("string"); + expect( + result.metadata === null || typeof result.metadata === "object", + ).toBe(true); + expect(Array.isArray(result.members)).toBe(true); + for (const m of result.members) { + expect(typeof m.id).toBe("string"); + expect(typeof m.flags).toBe("number"); + } + expect(typeof result.flags).toBe("number"); + }); + + it("error - NotFound for a non-existent lobby id", async () => { + await runEffect( + getLobby({ lobby_id: NON_EXISTENT_LOBBY_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may surface a missing lobby as NotFound, or as Forbidden + // when the application does not own the lobby. Some malformed + // snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a lobby the application cannot access", async () => { + await runEffect( + getLobby({ lobby_id: INACCESSIBLE_LOBBY_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getLobbyMessages.test.ts b/packages/discord/test/getLobbyMessages.test.ts new file mode 100644 index 000000000..ca24e07bf --- /dev/null +++ b/packages/discord/test/getLobbyMessages.test.ts @@ -0,0 +1,106 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createLobby } from "../src/operations/createLobby.ts"; +import { getLobbyMessages } from "../src/operations/getLobbyMessages.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /lobbies/{lobby_id}/messages — lists messages in a lobby. The bot's +// application must have the LOBBIES_WRITE scope. There is no deleteLobby +// endpoint; lobbies are reaped by `idle_timeout_seconds`. The list may +// legitimately be empty for a freshly-created lobby. +const TEST_LOBBY_ID = process.env.DISCORD_TEST_LOBBY_ID; + +// Snowflake-shaped ids unlikely to resolve to any real lobby. +const NON_EXISTENT_LOBBY_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_LOBBY_ID = "100000000000000001"; + +describe("getLobbyMessages", () => { + it("happy path - lists messages in a lobby", async () => { + let lobbyId = TEST_LOBBY_ID; + if (!lobbyId) { + const created = await runEffect( + createLobby({ + idle_timeout_seconds: 5, + metadata: { distilled_test: testRunId }, + }), + ); + lobbyId = created.id; + } + const result = await runEffect(getLobbyMessages({ lobby_id: lobbyId })); + expect(Array.isArray(result)).toBe(true); + for (const msg of result) { + expect(typeof msg.id).toBe("string"); + expect(typeof msg.content).toBe("string"); + expect(msg.lobby_id).toBe(lobbyId); + expect(typeof msg.channel_id).toBe("string"); + expect(typeof msg.author.id).toBe("string"); + expect(typeof msg.author.username).toBe("string"); + expect(typeof msg.flags).toBe("number"); + } + }); + + it("happy path - honors the limit query parameter", async () => { + let lobbyId = TEST_LOBBY_ID; + if (!lobbyId) { + const created = await runEffect( + createLobby({ + idle_timeout_seconds: 5, + metadata: { distilled_test: testRunId }, + }), + ); + lobbyId = created.id; + } + const result = await runEffect( + getLobbyMessages({ lobby_id: lobbyId, limit: 5 }), + ); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeLessThanOrEqual(5); + }); + + it("error - NotFound for a non-existent lobby id", async () => { + await runEffect( + getLobbyMessages({ lobby_id: NON_EXISTENT_LOBBY_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may surface a missing lobby as NotFound, or as Forbidden + // when the application does not own the lobby. Some malformed + // snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a lobby the application cannot access", async () => { + await runEffect( + getLobbyMessages({ lobby_id: INACCESSIBLE_LOBBY_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getMessage.test.ts b/packages/discord/test/getMessage.test.ts new file mode 100644 index 000000000..a7c17831c --- /dev/null +++ b/packages/discord/test/getMessage.test.ts @@ -0,0 +1,130 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createMessage } from "../src/operations/createMessage.ts"; +import { deleteMessage } from "../src/operations/deleteMessage.ts"; +import { getMessage } from "../src/operations/getMessage.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /channels/{channel_id}/messages/{message_id} fetches a single message. +// The bot must have VIEW_CHANNEL + READ_MESSAGE_HISTORY on the channel. +// Test setup posts a message and removes it via Effect.ensuring. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-shaped ids unlikely to resolve to any real message/channel. +const NON_EXISTENT_MESSAGE_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const NON_EXISTENT_CHANNEL_ID = "100000000000000001"; + +describe("getMessage", () => { + it("happy path - posts then fetches a message in the test channel", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID must be set for the getMessage happy path. " + + "The bot must have VIEW_CHANNEL, SEND_MESSAGES and READ_MESSAGE_HISTORY.", + ); + } + const created = await runEffect( + createMessage({ + channel_id: TEST_CHANNEL_ID, + content: `distilled-discord-getMessage-${testRunId}`, + }), + ); + try { + const result = await runEffect( + getMessage({ + channel_id: TEST_CHANNEL_ID, + // The created message id is in the response — created is the + // CreateMessageOutput which mirrors GetMessageOutput; both have + // `id`. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + message_id: (created as any).id, + }), + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(result.id).toBe((created as any).id); + expect(result.channel_id).toBe(TEST_CHANNEL_ID); + expect(typeof result.content).toBe("string"); + expect(result.content).toBe( + `distilled-discord-getMessage-${testRunId}`, + ); + expect(typeof result.author.id).toBe("string"); + expect(typeof result.author.username).toBe("string"); + expect(typeof result.timestamp).toBe("string"); + expect(typeof result.flags).toBe("number"); + expect(Array.isArray(result.mentions)).toBe(true); + expect(Array.isArray(result.attachments)).toBe(true); + expect(Array.isArray(result.embeds)).toBe(true); + expect(typeof result.pinned).toBe("boolean"); + expect(typeof result.tts).toBe("boolean"); + expect(typeof result.mention_everyone).toBe("boolean"); + } finally { + await runEffect( + deleteMessage({ + channel_id: TEST_CHANNEL_ID, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + message_id: (created as any).id, + }).pipe(Effect.ignore), + ); + } + }); + + it("error - NotFound for a non-existent message id", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID must be set for the getMessage error tests.", + ); + } + await runEffect( + getMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound (10008 — Unknown Message) for missing + // message ids. Some malformed snowflakes may surface as + // BadRequest, and revoked access as Forbidden. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a channel the bot cannot access", async () => { + await runEffect( + getMessage({ + channel_id: NON_EXISTENT_CHANNEL_ID, + message_id: "100000000000000002", + }).pipe( + Effect.flip, + Effect.map((e) => { + // For a missing or inaccessible channel, Discord prefers Forbidden + // (Missing Access) when the channel exists but the bot can't view + // it, and NotFound when the channel does not exist. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getMyApplication.test.ts b/packages/discord/test/getMyApplication.test.ts new file mode 100644 index 000000000..9f3469925 --- /dev/null +++ b/packages/discord/test/getMyApplication.test.ts @@ -0,0 +1,126 @@ +import { config } from "dotenv"; +import { Effect, Layer, Redacted } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getMyApplication } from "../src/operations/getMyApplication.ts"; +import { + Credentials, + CredentialsFromEnv, + DEFAULT_API_BASE_URL, +} from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// A layer that supplies plausible-looking but invalid credentials so the +// /applications/@me endpoint rejects the request. The endpoint has no input +// parameters, so error cases (NotFound / Forbidden) can only be reached by +// manipulating the auth context — Discord rejects unknown tokens at the +// auth layer before any path-routing checks run. +const bogusCredentialsLayer = ( + scheme: "Bot" | "Bearer", +): Layer.Layer => + Layer.succeed(Credentials, { + token: Redacted.make( + "MTAwMDAwMDAwMDAwMDAwMDAw.bogus.token-for-distilled-tests", + ), + authScheme: scheme, + apiBaseUrl: process.env.DISCORD_API_BASE_URL ?? DEFAULT_API_BASE_URL, + }); + +const runWithBogusCreds = ( + effect: Effect.Effect, + scheme: "Bot" | "Bearer", +): Promise => { + const layer = Layer.merge( + bogusCredentialsLayer(scheme), + FetchHttpClient.layer, + ); + return Effect.runPromise( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + effect.pipe(Effect.provide(layer)) as Effect.Effect, + ); +}; + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +describe("getMyApplication", () => { + it("happy path - returns the bot's own application object", async () => { + const result = await runEffect(getMyApplication({})); + expect(typeof result.id).toBe("string"); + expect(result.id.length).toBeGreaterThan(0); + expect(typeof result.name).toBe("string"); + expect(result.icon === null || typeof result.icon === "string").toBe(true); + expect(typeof result.description).toBe("string"); + expect(typeof result.verify_key).toBe("string"); + expect(typeof result.flags).toBe("number"); + expect(typeof result.flags_new).toBe("string"); + expect(Array.isArray(result.redirect_uris)).toBe(true); + expect( + result.interactions_endpoint_url === null || + typeof result.interactions_endpoint_url === "string", + ).toBe(true); + expect( + result.role_connections_verification_url === null || + typeof result.role_connections_verification_url === "string", + ).toBe(true); + expect(typeof result.owner.id).toBe("string"); + expect(typeof result.owner.username).toBe("string"); + expect(typeof result.approximate_guild_count).toBe("number"); + expect(typeof result.approximate_user_install_count).toBe("number"); + expect(typeof result.approximate_user_authorization_count).toBe("number"); + }); + + it("error - NotFound / Forbidden surface when the bot token is rejected", async () => { + // /applications/@me has no path params — the only way to trigger the + // listed typed errors is to send an unrecognized token. Discord + // commonly responds with 401 for invalid tokens (mapped to + // Unauthorized) but may also classify the request as 404/403 depending + // on routing. + await runWithBogusCreds( + getMyApplication({}).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "NotFound", + "Forbidden", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + "Bot", + ); + }); + + it("error - Forbidden when a Bearer token is used on a bot-only route", async () => { + // /applications/@me is a bot-token-only route. Calling it with a Bearer + // credential typically yields 403 Forbidden, but Discord may also + // return 401 Unauthorized depending on token validity, or 404 if route + // resolution falls through ahead of the permission check. + await runWithBogusCreds( + getMyApplication({}).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "Forbidden", + "NotFound", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + "Bearer", + ); + }); +}); diff --git a/packages/discord/test/getMyGuildMember.test.ts b/packages/discord/test/getMyGuildMember.test.ts new file mode 100644 index 000000000..aad5c37ca --- /dev/null +++ b/packages/discord/test/getMyGuildMember.test.ts @@ -0,0 +1,120 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getMyGuildMember } from "../src/operations/getMyGuildMember.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /users/@me/guilds/{guild_id}/member is an OAuth2-only route. It +// requires a Bearer token with the `guilds.members.read` scope; bot tokens +// cannot use any /users/@me endpoint. The happy path therefore requires the +// SDK to be configured with DISCORD_BEARER_TOKEN (CredentialsFromEnv flips +// to Bearer scheme automatically when that env var is set) and +// DISCORD_TEST_GUILD_ID for a guild the bearer token's user is a member of. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const HAS_BEARER = Boolean(process.env.DISCORD_BEARER_TOKEN); + +// Snowflake-shaped ids unlikely to resolve to a guild the user belongs to. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("getMyGuildMember", () => { + it("happy path - returns the current user's member object for a guild", async () => { + if (!HAS_BEARER) { + throw new Error( + "DISCORD_BEARER_TOKEN must be set for the getMyGuildMember happy path. " + + "The token must be an OAuth2 Bearer with the `guilds.members.read` scope.", + ); + } + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID must be set for the getMyGuildMember happy path. " + + "The bearer token's user must be a member of this guild.", + ); + } + const result = await runEffect( + getMyGuildMember({ guild_id: TEST_GUILD_ID }), + ); + expect(result.avatar === null || typeof result.avatar === "string").toBe( + true, + ); + expect(result.banner === null || typeof result.banner === "string").toBe( + true, + ); + expect( + result.communication_disabled_until === null || + typeof result.communication_disabled_until === "string", + ).toBe(true); + expect(typeof result.flags).toBe("number"); + expect(typeof result.joined_at).toBe("string"); + expect(result.nick === null || typeof result.nick === "string").toBe(true); + expect(typeof result.pending).toBe("boolean"); + expect( + result.premium_since === null || + typeof result.premium_since === "string", + ).toBe(true); + expect(Array.isArray(result.roles)).toBe(true); + for (const role of result.roles) { + expect(typeof role).toBe("string"); + } + expect(typeof result.user.id).toBe("string"); + expect(typeof result.user.username).toBe("string"); + expect(typeof result.mute).toBe("boolean"); + expect(typeof result.deaf).toBe("boolean"); + }); + + it("error - NotFound for a non-existent guild id", async () => { + if (!HAS_BEARER) { + throw new Error( + "DISCORD_BEARER_TOKEN must be set for the getMyGuildMember error tests.", + ); + } + await runEffect( + getMyGuildMember({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may surface a missing guild as NotFound (10004), or as + // Forbidden when the user is not a member of the guild. Some + // malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the user is not in", async () => { + if (!HAS_BEARER) { + throw new Error( + "DISCORD_BEARER_TOKEN must be set for the getMyGuildMember error tests.", + ); + } + await runEffect( + getMyGuildMember({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getMyOauth2Application.test.ts b/packages/discord/test/getMyOauth2Application.test.ts new file mode 100644 index 000000000..06e8f2c8a --- /dev/null +++ b/packages/discord/test/getMyOauth2Application.test.ts @@ -0,0 +1,125 @@ +import { config } from "dotenv"; +import { Effect, Layer, Redacted } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getMyOauth2Application } from "../src/operations/getMyOauth2Application.ts"; +import { + Credentials, + CredentialsFromEnv, + DEFAULT_API_BASE_URL, +} from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// Layer that supplies plausible-looking but invalid credentials so the +// /oauth2/applications/@me endpoint rejects the request. The endpoint takes +// no input parameters, so error cases (NotFound / Forbidden) can only be +// reached by manipulating the auth context. +const bogusCredentialsLayer = ( + scheme: "Bot" | "Bearer", +): Layer.Layer => + Layer.succeed(Credentials, { + token: Redacted.make( + "MTAwMDAwMDAwMDAwMDAwMDAw.bogus.token-for-distilled-tests", + ), + authScheme: scheme, + apiBaseUrl: process.env.DISCORD_API_BASE_URL ?? DEFAULT_API_BASE_URL, + }); + +const runWithBogusCreds = ( + effect: Effect.Effect, + scheme: "Bot" | "Bearer", +): Promise => { + const layer = Layer.merge( + bogusCredentialsLayer(scheme), + FetchHttpClient.layer, + ); + return Effect.runPromise( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + effect.pipe(Effect.provide(layer)) as Effect.Effect, + ); +}; + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +describe("getMyOauth2Application", () => { + it("happy path - returns the OAuth2 application object for the bot token", async () => { + const result = await runEffect(getMyOauth2Application({})); + expect(typeof result.id).toBe("string"); + expect(result.id.length).toBeGreaterThan(0); + expect(typeof result.name).toBe("string"); + expect(result.icon === null || typeof result.icon === "string").toBe(true); + expect(typeof result.description).toBe("string"); + expect(typeof result.verify_key).toBe("string"); + expect(typeof result.flags).toBe("number"); + expect(typeof result.flags_new).toBe("string"); + expect(Array.isArray(result.redirect_uris)).toBe(true); + expect( + result.interactions_endpoint_url === null || + typeof result.interactions_endpoint_url === "string", + ).toBe(true); + expect( + result.role_connections_verification_url === null || + typeof result.role_connections_verification_url === "string", + ).toBe(true); + expect(typeof result.owner.id).toBe("string"); + expect(typeof result.owner.username).toBe("string"); + expect(typeof result.approximate_guild_count).toBe("number"); + expect(typeof result.approximate_user_install_count).toBe("number"); + expect(typeof result.approximate_user_authorization_count).toBe("number"); + }); + + it("error - NotFound / Forbidden surface when the bot token is rejected", async () => { + // /oauth2/applications/@me has no path params — the only way to trigger + // the listed typed errors is to send an unrecognized token. Discord + // commonly responds with 401 for invalid tokens (mapped to + // Unauthorized) but may also classify the request as 404/403 depending + // on routing. + await runWithBogusCreds( + getMyOauth2Application({}).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "NotFound", + "Forbidden", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + "Bot", + ); + }); + + it("error - Forbidden when a Bearer token is rejected on the route", async () => { + // /oauth2/applications/@me is conventionally a bot-only route; using a + // Bearer credential typically yields 403 Forbidden, but Discord may + // also return 401 Unauthorized depending on token validity, or 404 if + // route resolution falls through ahead of the permission check. + await runWithBogusCreds( + getMyOauth2Application({}).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "Forbidden", + "NotFound", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + "Bearer", + ); + }); +}); diff --git a/packages/discord/test/getMyOauth2Authorization.test.ts b/packages/discord/test/getMyOauth2Authorization.test.ts new file mode 100644 index 000000000..62f5a2812 --- /dev/null +++ b/packages/discord/test/getMyOauth2Authorization.test.ts @@ -0,0 +1,126 @@ +import { config } from "dotenv"; +import { Effect, Layer, Redacted } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getMyOauth2Authorization } from "../src/operations/getMyOauth2Authorization.ts"; +import { + Credentials, + CredentialsFromEnv, + DEFAULT_API_BASE_URL, +} from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// Layer that supplies plausible-looking but invalid credentials so the +// /oauth2/@me endpoint rejects the request. The endpoint takes no input +// parameters, so error cases can only be reached by manipulating the auth +// context. +const bogusCredentialsLayer = ( + scheme: "Bot" | "Bearer", +): Layer.Layer => + Layer.succeed(Credentials, { + token: Redacted.make( + "MTAwMDAwMDAwMDAwMDAwMDAw.bogus.token-for-distilled-tests", + ), + authScheme: scheme, + apiBaseUrl: process.env.DISCORD_API_BASE_URL ?? DEFAULT_API_BASE_URL, + }); + +const runWithBogusCreds = ( + effect: Effect.Effect, + scheme: "Bot" | "Bearer", +): Promise => { + const layer = Layer.merge( + bogusCredentialsLayer(scheme), + FetchHttpClient.layer, + ); + return Effect.runPromise( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + effect.pipe(Effect.provide(layer)) as Effect.Effect, + ); +}; + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +// /oauth2/@me requires a Bearer access token (NOT a bot token). The happy +// path is gated behind DISCORD_BEARER_TOKEN being set; CredentialsFromEnv +// switches to Bearer scheme automatically when that env var is present. +const HAS_BEARER = Boolean(process.env.DISCORD_BEARER_TOKEN); + +describe("getMyOauth2Authorization", () => { + it("happy path - returns the current bearer-token authorization", async () => { + if (!HAS_BEARER) { + throw new Error( + "DISCORD_BEARER_TOKEN must be set for the getMyOauth2Authorization happy path. " + + "The token must be an OAuth2 Bearer access token (not a bot token).", + ); + } + const result = await runEffect(getMyOauth2Authorization({})); + expect(typeof result.application.id).toBe("string"); + expect(typeof result.application.name).toBe("string"); + expect(typeof result.application.description).toBe("string"); + expect(typeof result.application.verify_key).toBe("string"); + expect(typeof result.application.flags).toBe("number"); + expect(typeof result.application.flags_new).toBe("string"); + expect(typeof result.expires).toBe("string"); + expect(Array.isArray(result.scopes)).toBe(true); + expect(result.scopes.length).toBeGreaterThan(0); + if (result.user) { + expect(typeof result.user.id).toBe("string"); + expect(typeof result.user.username).toBe("string"); + } + }); + + it("error - NotFound / Forbidden surface when a Bot token is used", async () => { + // /oauth2/@me requires a Bearer access token. Sending a Bot token (or + // any unrecognized token) results in 401 Unauthorized in most cases, + // but Discord may also classify the request as 403/404 depending on + // routing. + await runWithBogusCreds( + getMyOauth2Authorization({}).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "NotFound", + "Forbidden", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + "Bot", + ); + }); + + it("error - Forbidden / NotFound when the Bearer token is rejected", async () => { + // Sending a malformed Bearer access token typically yields 401 + // Unauthorized, but Discord may also surface 403 Forbidden or 404 + // NotFound depending on how the route resolves before the auth check + // completes. + await runWithBogusCreds( + getMyOauth2Authorization({}).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "Forbidden", + "NotFound", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + "Bearer", + ); + }); +}); diff --git a/packages/discord/test/getMyUser.test.ts b/packages/discord/test/getMyUser.test.ts new file mode 100644 index 000000000..befbb9910 --- /dev/null +++ b/packages/discord/test/getMyUser.test.ts @@ -0,0 +1,115 @@ +import { config } from "dotenv"; +import { Effect, Layer, Redacted } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getMyUser } from "../src/operations/getMyUser.ts"; +import { + Credentials, + CredentialsFromEnv, + DEFAULT_API_BASE_URL, +} from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// Layer that supplies plausible-looking but invalid credentials so the +// /users/@me endpoint rejects the request. The endpoint takes no input +// parameters, so error cases (NotFound / Forbidden) can only be reached by +// manipulating the auth context. +const bogusCredentialsLayer = ( + scheme: "Bot" | "Bearer", +): Layer.Layer => + Layer.succeed(Credentials, { + token: Redacted.make( + "MTAwMDAwMDAwMDAwMDAwMDAw.bogus.token-for-distilled-tests", + ), + authScheme: scheme, + apiBaseUrl: process.env.DISCORD_API_BASE_URL ?? DEFAULT_API_BASE_URL, + }); + +const runWithBogusCreds = ( + effect: Effect.Effect, + scheme: "Bot" | "Bearer", +): Promise => { + const layer = Layer.merge( + bogusCredentialsLayer(scheme), + FetchHttpClient.layer, + ); + return Effect.runPromise( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + effect.pipe(Effect.provide(layer)) as Effect.Effect, + ); +}; + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +describe("getMyUser", () => { + it("happy path - returns the current user (bot or OAuth2 identity)", async () => { + const result = await runEffect(getMyUser({})); + expect(typeof result.id).toBe("string"); + expect(result.id.length).toBeGreaterThan(0); + expect(typeof result.username).toBe("string"); + expect(result.avatar === null || typeof result.avatar === "string").toBe( + true, + ); + expect(typeof result.discriminator).toBe("string"); + expect(typeof result.public_flags).toBe("number"); + expect(typeof result.flags).toBe("number"); + expect( + result.global_name === null || typeof result.global_name === "string", + ).toBe(true); + expect(typeof result.mfa_enabled).toBe("boolean"); + }); + + it("error - NotFound / Forbidden surface when the bot token is rejected", async () => { + // /users/@me has no path params — the only way to trigger the listed + // typed errors is to send an unrecognized token. Discord commonly + // responds with 401 for invalid tokens (mapped to Unauthorized) but + // may also classify the request as 404/403 depending on routing. + await runWithBogusCreds( + getMyUser({}).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "NotFound", + "Forbidden", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + "Bot", + ); + }); + + it("error - Forbidden / NotFound when a Bearer token is rejected", async () => { + // Sending a malformed Bearer access token typically yields 401 + // Unauthorized, but Discord may also surface 403 Forbidden or 404 + // NotFound depending on how the route resolves before the auth check + // completes. + await runWithBogusCreds( + getMyUser({}).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "Forbidden", + "NotFound", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + "Bearer", + ); + }); +}); diff --git a/packages/discord/test/getOpenidConnectUserinfo.test.ts b/packages/discord/test/getOpenidConnectUserinfo.test.ts new file mode 100644 index 000000000..8004a9055 --- /dev/null +++ b/packages/discord/test/getOpenidConnectUserinfo.test.ts @@ -0,0 +1,139 @@ +import { config } from "dotenv"; +import { Effect, Layer, Redacted } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getOpenidConnectUserinfo } from "../src/operations/getOpenidConnectUserinfo.ts"; +import { + Credentials, + CredentialsFromEnv, + DEFAULT_API_BASE_URL, +} from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// Layer that supplies plausible-looking but invalid credentials so the +// /oauth2/userinfo endpoint rejects the request. The endpoint takes no +// input parameters, so error cases can only be reached by manipulating the +// auth context. +const bogusCredentialsLayer = ( + scheme: "Bot" | "Bearer", +): Layer.Layer => + Layer.succeed(Credentials, { + token: Redacted.make( + "MTAwMDAwMDAwMDAwMDAwMDAw.bogus.token-for-distilled-tests", + ), + authScheme: scheme, + apiBaseUrl: process.env.DISCORD_API_BASE_URL ?? DEFAULT_API_BASE_URL, + }); + +const runWithBogusCreds = ( + effect: Effect.Effect, + scheme: "Bot" | "Bearer", +): Promise => { + const layer = Layer.merge( + bogusCredentialsLayer(scheme), + FetchHttpClient.layer, + ); + return Effect.runPromise( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + effect.pipe(Effect.provide(layer)) as Effect.Effect, + ); +}; + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +// /oauth2/userinfo is the OpenID Connect UserInfo endpoint. It requires a +// Bearer access token issued with the `openid` scope; bot tokens are +// rejected. The happy path is gated behind DISCORD_BEARER_TOKEN being set +// (CredentialsFromEnv switches to Bearer scheme automatically when that env +// var is present). +const HAS_BEARER = Boolean(process.env.DISCORD_BEARER_TOKEN); + +describe("getOpenidConnectUserinfo", () => { + it("happy path - returns the OIDC user info for the bearer token", async () => { + if (!HAS_BEARER) { + throw new Error( + "DISCORD_BEARER_TOKEN must be set for the getOpenidConnectUserinfo happy path. " + + "The token must be an OAuth2 Bearer with the `openid` scope.", + ); + } + const result = await runEffect(getOpenidConnectUserinfo({})); + expect(typeof result.sub).toBe("string"); + expect(result.sub.length).toBeGreaterThan(0); + if (result.email !== undefined) { + expect(result.email === null || typeof result.email === "string").toBe( + true, + ); + } + if (result.email_verified !== undefined) { + expect(typeof result.email_verified).toBe("boolean"); + } + if (result.preferred_username !== undefined) { + expect(typeof result.preferred_username).toBe("string"); + } + if (result.nickname !== undefined) { + expect( + result.nickname === null || typeof result.nickname === "string", + ).toBe(true); + } + if (result.picture !== undefined) { + expect(typeof result.picture).toBe("string"); + } + if (result.locale !== undefined) { + expect(typeof result.locale).toBe("string"); + } + }); + + it("error - NotFound / Forbidden surface when a Bot token is used", async () => { + // /oauth2/userinfo requires a Bearer access token with the `openid` + // scope. Sending a Bot token results in 401 Unauthorized in most + // cases, but Discord may also classify the request as 403/404 + // depending on routing. + await runWithBogusCreds( + getOpenidConnectUserinfo({}).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "NotFound", + "Forbidden", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + "Bot", + ); + }); + + it("error - Forbidden / NotFound when the Bearer token is rejected", async () => { + // Sending a malformed Bearer access token typically yields 401 + // Unauthorized, but Discord may also surface 403 Forbidden or 404 + // NotFound depending on how the route resolves before the auth check + // completes. + await runWithBogusCreds( + getOpenidConnectUserinfo({}).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "Forbidden", + "NotFound", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + "Bearer", + ); + }); +}); diff --git a/packages/discord/test/getOriginalWebhookMessage.test.ts b/packages/discord/test/getOriginalWebhookMessage.test.ts new file mode 100644 index 000000000..a852d7e60 --- /dev/null +++ b/packages/discord/test/getOriginalWebhookMessage.test.ts @@ -0,0 +1,161 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getOriginalWebhookMessage } from "../src/operations/getOriginalWebhookMessage.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /webhooks/{webhook_id}/{webhook_token}/messages/@original — fetches +// the original message for a webhook execution / interaction. The webhook +// path is unauthenticated (the token in the path is the credential); the +// webhook_id must point at a real webhook and the token must match. +// +// `@original` resolves to the most recently posted message for the webhook +// or interaction token. The happy path therefore requires operator-supplied +// env vars pointing at a webhook that has at least one message posted via +// that token. +const TEST_WEBHOOK_ID = process.env.DISCORD_TEST_WEBHOOK_ID; +const TEST_WEBHOOK_TOKEN = process.env.DISCORD_TEST_WEBHOOK_TOKEN; + +// Snowflake-shaped id and a randomly-generated token unlikely to match any +// real webhook. Discord typically returns 404 (10015 — Unknown Webhook) for +// missing ids, and 401/403 for token mismatches. +const NON_EXISTENT_WEBHOOK_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const NON_EXISTENT_WEBHOOK_TOKEN = `distilled-bogus-webhook-token-${testRunId}`; + +describe("getOriginalWebhookMessage", () => { + it("happy path - fetches the original webhook message", async () => { + if (!TEST_WEBHOOK_ID || !TEST_WEBHOOK_TOKEN) { + throw new Error( + "DISCORD_TEST_WEBHOOK_ID and DISCORD_TEST_WEBHOOK_TOKEN must be set " + + "for the getOriginalWebhookMessage happy path. The webhook must have " + + "at least one message posted via its token.", + ); + } + const result = await runEffect( + getOriginalWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: TEST_WEBHOOK_TOKEN, + }), + ); + expect(typeof result.id).toBe("string"); + expect(typeof result.channel_id).toBe("string"); + expect(typeof result.content).toBe("string"); + expect(typeof result.timestamp).toBe("string"); + expect( + result.edited_timestamp === null || + typeof result.edited_timestamp === "string", + ).toBe(true); + expect(typeof result.flags).toBe("number"); + expect(Array.isArray(result.mentions)).toBe(true); + expect(Array.isArray(result.mention_roles)).toBe(true); + expect(Array.isArray(result.attachments)).toBe(true); + expect(Array.isArray(result.embeds)).toBe(true); + expect(Array.isArray(result.components)).toBe(true); + expect(typeof result.author.id).toBe("string"); + expect(typeof result.author.username).toBe("string"); + expect(typeof result.pinned).toBe("boolean"); + expect(typeof result.mention_everyone).toBe("boolean"); + expect(typeof result.tts).toBe("boolean"); + }); + + it("happy path - accepts the optional thread_id query parameter", async () => { + if (!TEST_WEBHOOK_ID || !TEST_WEBHOOK_TOKEN) { + throw new Error( + "DISCORD_TEST_WEBHOOK_ID and DISCORD_TEST_WEBHOOK_TOKEN must be set " + + "for the getOriginalWebhookMessage happy path.", + ); + } + // Optional thread_id — if the operator supplies a thread id where the + // webhook posted the original, use it; otherwise omit the param. + const threadId = process.env.DISCORD_TEST_WEBHOOK_THREAD_ID; + if (!threadId) { + // Without a thread_id, the call is the same as the previous test; + // re-running it here would be redundant. Skip silently. + return; + } + const result = await runEffect( + getOriginalWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: TEST_WEBHOOK_TOKEN, + thread_id: threadId, + }), + ); + expect(typeof result.id).toBe("string"); + expect(typeof result.content).toBe("string"); + }); + + it("error - NotFound for a non-existent webhook id", async () => { + await runEffect( + getOriginalWebhookMessage({ + webhook_id: NON_EXISTENT_WEBHOOK_ID, + webhook_token: NON_EXISTENT_WEBHOOK_TOKEN, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord typically returns NotFound (10015 — Unknown Webhook) for + // missing webhook ids. A token mismatch on a real webhook id may + // surface as Forbidden, and malformed snowflakes as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a token mismatch", async () => { + if (!TEST_WEBHOOK_ID) { + // Without a real webhook id, fall back to the missing-id case. + await runEffect( + getOriginalWebhookMessage({ + webhook_id: "100000000000000001", + webhook_token: NON_EXISTENT_WEBHOOK_TOKEN, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + return; + } + // Real webhook id + bogus token typically yields 401 or 403; some + // routes resolve as 404 instead. + await runEffect( + getOriginalWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: NON_EXISTENT_WEBHOOK_TOKEN, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "NotFound", + "Forbidden", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getPublicKeys.test.ts b/packages/discord/test/getPublicKeys.test.ts new file mode 100644 index 000000000..99d4bb6d0 --- /dev/null +++ b/packages/discord/test/getPublicKeys.test.ts @@ -0,0 +1,102 @@ +import { config } from "dotenv"; +import { Effect, Layer, Redacted } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getPublicKeys } from "../src/operations/getPublicKeys.ts"; +import { Credentials, CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// A layer that points the SDK at a Discord-shaped base URL whose route does +// not exist. /oauth2/keys has no input parameters, so error cases +// (NotFound / Forbidden) can only be reached by manipulating the request +// context. +const customBaseUrlLayer = (apiBaseUrl: string): Layer.Layer => + Layer.succeed(Credentials, { + token: Redacted.make( + "MTAwMDAwMDAwMDAwMDAwMDAw.bogus.token-for-distilled-tests", + ), + authScheme: "Bot" as const, + apiBaseUrl, + }); + +const runWithBaseUrl = ( + effect: Effect.Effect, + apiBaseUrl: string, +): Promise => { + const layer = Layer.merge( + customBaseUrlLayer(apiBaseUrl), + FetchHttpClient.layer, + ); + return Effect.runPromise( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + effect.pipe(Effect.provide(layer)) as Effect.Effect, + ); +}; + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +describe("getPublicKeys", () => { + it("happy path - returns the JWKS public keys for OIDC token verification", async () => { + // /oauth2/keys is a public, unauthenticated JWKS endpoint. + const result = await runEffect(getPublicKeys({})); + expect(Array.isArray(result.keys)).toBe(true); + expect(result.keys.length).toBeGreaterThan(0); + for (const key of result.keys) { + expect(typeof key.kty).toBe("string"); + expect(typeof key.use).toBe("string"); + expect(typeof key.kid).toBe("string"); + expect(typeof key.n).toBe("string"); + expect(typeof key.e).toBe("string"); + expect(typeof key.alg).toBe("string"); + } + }); + + it("error - NotFound when /oauth2/keys is unrouted on the configured base URL", async () => { + // /oauth2/keys has no path params; the only realistic way to surface a + // 404 is to point the SDK at a Discord-shaped base URL with a + // non-existent API version. Discord returns 404 for /api/v999/... + await runWithBaseUrl( + getPublicKeys({}).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + "https://discord.com/api/v999", + ); + }); + + it("error - Forbidden when the host classifies the request as forbidden", async () => { + // Pointing at a host path that responds 403 (or 404) for unknown + // routes exercises the SDK's Forbidden / NotFound mapping. The typed + // tag set is tolerant because hosts may return 404 / 400 instead of + // 403. + await runWithBaseUrl( + getPublicKeys({}).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + "https://discord.com/forbidden-distilled-test", + ); + }); +}); diff --git a/packages/discord/test/getSelfVoiceState.test.ts b/packages/discord/test/getSelfVoiceState.test.ts new file mode 100644 index 000000000..aa2b6dccf --- /dev/null +++ b/packages/discord/test/getSelfVoiceState.test.ts @@ -0,0 +1,97 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getSelfVoiceState } from "../src/operations/getSelfVoiceState.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/voice-states/@me — returns the *bot's* current +// voice state in the given guild. The bot must be connected to a voice +// channel in that guild; if it isn't, Discord returns 404 (Voice State Not +// Found). The happy path therefore requires the operator to supply +// DISCORD_TEST_VOICE_GUILD_ID — a guild where the bot is actively in voice. +const TEST_VOICE_GUILD_ID = process.env.DISCORD_TEST_VOICE_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to a guild the bot is in voice +// in. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("getSelfVoiceState", () => { + it("happy path - returns the bot's voice state in the guild", async () => { + if (!TEST_VOICE_GUILD_ID) { + throw new Error( + "DISCORD_TEST_VOICE_GUILD_ID must be set for the getSelfVoiceState happy path. " + + "The bot must be actively connected to a voice channel in this guild.", + ); + } + const result = await runEffect( + getSelfVoiceState({ guild_id: TEST_VOICE_GUILD_ID }), + ); + // channel_id and guild_id are typed as `Schema.Unknown` (Discord may + // return them as snowflake strings or null). Assert the property is + // present rather than a specific shape. + expect("channel_id" in result).toBe(true); + expect("guild_id" in result).toBe(true); + expect(typeof result.deaf).toBe("boolean"); + expect(typeof result.mute).toBe("boolean"); + expect( + result.request_to_speak_timestamp === null || + typeof result.request_to_speak_timestamp === "string", + ).toBe(true); + expect(typeof result.suppress).toBe("boolean"); + expect( + result.self_stream === null || typeof result.self_stream === "boolean", + ).toBe(true); + expect(typeof result.self_deaf).toBe("boolean"); + expect(typeof result.self_mute).toBe("boolean"); + expect(typeof result.self_video).toBe("boolean"); + expect(typeof result.session_id).toBe("string"); + expect(typeof result.user_id).toBe("string"); + }); + + it("error - NotFound for a non-existent guild id", async () => { + await runEffect( + getSelfVoiceState({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may surface a missing guild as NotFound (10004), or as + // Forbidden (Missing Access) when the bot is not in the guild. + // Some malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot cannot access", async () => { + await runEffect( + getSelfVoiceState({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getSoundboardDefaultSounds.test.ts b/packages/discord/test/getSoundboardDefaultSounds.test.ts new file mode 100644 index 000000000..7ef0c278f --- /dev/null +++ b/packages/discord/test/getSoundboardDefaultSounds.test.ts @@ -0,0 +1,109 @@ +import { config } from "dotenv"; +import { Effect, Layer, Redacted } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getSoundboardDefaultSounds } from "../src/operations/getSoundboardDefaultSounds.ts"; +import { Credentials, CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// A layer that points the SDK at a Discord-shaped base URL whose route +// does not exist. /soundboard-default-sounds has no input parameters, so +// error cases (NotFound / Forbidden) can only be reached by manipulating +// the request context. +const customBaseUrlLayer = (apiBaseUrl: string): Layer.Layer => + Layer.succeed(Credentials, { + token: Redacted.make( + "MTAwMDAwMDAwMDAwMDAwMDAw.bogus.token-for-distilled-tests", + ), + authScheme: "Bot" as const, + apiBaseUrl, + }); + +const runWithBaseUrl = ( + effect: Effect.Effect, + apiBaseUrl: string, +): Promise => { + const layer = Layer.merge( + customBaseUrlLayer(apiBaseUrl), + FetchHttpClient.layer, + ); + return Effect.runPromise( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + effect.pipe(Effect.provide(layer)) as Effect.Effect, + ); +}; + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +void testRunId; + +describe("getSoundboardDefaultSounds", () => { + it("happy path - returns the list of default soundboard sounds", async () => { + const result = await runEffect(getSoundboardDefaultSounds({})); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeGreaterThan(0); + for (const sound of result) { + expect(typeof sound.name).toBe("string"); + expect(typeof sound.sound_id).toBe("string"); + expect(typeof sound.volume).toBe("number"); + expect( + sound.emoji_name === null || typeof sound.emoji_name === "string", + ).toBe(true); + expect(typeof sound.available).toBe("boolean"); + if (sound.guild_id !== undefined) { + expect(typeof sound.guild_id).toBe("string"); + } + if (sound.user !== undefined) { + expect(typeof sound.user.id).toBe("string"); + expect(typeof sound.user.username).toBe("string"); + } + } + }); + + it("error - NotFound when /soundboard-default-sounds is unrouted on the configured base URL", async () => { + // The endpoint has no path params; the only realistic way to surface a + // 404 is to point the SDK at a Discord-shaped base URL with a + // non-existent API version. Discord returns 404 for /api/v999/... + await runWithBaseUrl( + getSoundboardDefaultSounds({}).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + "https://discord.com/api/v999", + ); + }); + + it("error - Forbidden when the host classifies the request as forbidden", async () => { + // Pointing at a host path that responds 403 (or 404) for unknown + // routes exercises the SDK's Forbidden / NotFound mapping. The typed + // tag set is tolerant because hosts may return 404 / 400 instead of + // 403. + await runWithBaseUrl( + getSoundboardDefaultSounds({}).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + "https://discord.com/forbidden-distilled-test", + ); + }); +}); diff --git a/packages/discord/test/getStageInstance.test.ts b/packages/discord/test/getStageInstance.test.ts new file mode 100644 index 000000000..1cf877043 --- /dev/null +++ b/packages/discord/test/getStageInstance.test.ts @@ -0,0 +1,101 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createStageInstance } from "../src/operations/createStageInstance.ts"; +import { deleteStageInstance } from "../src/operations/deleteStageInstance.ts"; +import { getStageInstance } from "../src/operations/getStageInstance.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /stage-instances/{channel_id} requires a live stage instance, which can +// only exist on a Stage Channel (channel type 13). The bot must have +// MANAGE_CHANNELS, MUTE_MEMBERS, and MOVE_MEMBERS in the channel. Stage +// channels cannot be cleanly created via the public API in a test fixture, +// so the happy path is gated on an operator-supplied DISCORD_TEST_STAGE_CHANNEL_ID +// pointing at a stage channel the bot can manage. +const TEST_STAGE_CHANNEL_ID = process.env.DISCORD_TEST_STAGE_CHANNEL_ID; + +// Snowflake-shaped ids unlikely to resolve to any real stage channel. +const NON_EXISTENT_CHANNEL_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_CHANNEL_ID = "100000000000000001"; + +describe("getStageInstance", () => { + it( + "happy path - fetches a stage instance", + async () => { + if (!TEST_STAGE_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_STAGE_CHANNEL_ID is required for the getStageInstance happy path. " + + "Set it to the id of a Stage Channel (type 13) the bot can manage.", + ); + } + const channelId = TEST_STAGE_CHANNEL_ID; + const topic = `distilled-discord-stage-${testRunId}`; + await runEffect( + Effect.gen(function* () { + yield* createStageInstance({ + channel_id: channelId, + topic, + }); + const result = yield* getStageInstance({ channel_id: channelId }); + expect(typeof result.id).toBe("string"); + expect(result.channel_id).toBe(channelId); + expect(typeof result.guild_id).toBe("string"); + expect(typeof result.topic).toBe("string"); + expect(typeof result.discoverable_disabled).toBe("boolean"); + }).pipe( + Effect.ensuring( + deleteStageInstance({ channel_id: channelId }).pipe(Effect.ignore), + ), + ), + ); + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent stage channel id", async () => { + await runEffect( + getStageInstance({ channel_id: NON_EXISTENT_CHANNEL_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing stage instance as NotFound. Some + // malformed or out-of-range snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a stage channel the bot cannot access", async () => { + await runEffect( + getStageInstance({ channel_id: INACCESSIBLE_CHANNEL_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // A channel id the bot is not in typically surfaces as Forbidden, + // but Discord often returns NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getSticker.test.ts b/packages/discord/test/getSticker.test.ts new file mode 100644 index 000000000..2b4984bed --- /dev/null +++ b/packages/discord/test/getSticker.test.ts @@ -0,0 +1,86 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getSticker } from "../src/operations/getSticker.ts"; +import { listStickerPacks } from "../src/operations/listStickerPacks.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /stickers/{sticker_id} resolves any sticker visible to the bot. Standard +// Discord sticker-pack stickers are public, so the happy path resolves a real +// sticker id by listing sticker packs and reading the first sticker. + +// Snowflake-shaped ids unlikely to resolve to any real sticker. +const NON_EXISTENT_STICKER_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_STICKER_ID = "100000000000000001"; + +describe("getSticker", () => { + it( + "happy path - fetches a standard sticker", + async () => { + const packs = await runEffect(listStickerPacks({})); + const firstPack = packs.sticker_packs.find((p) => p.stickers.length > 0); + if (!firstPack) { + throw new Error( + "No sticker packs with stickers were returned by listStickerPacks; " + + "cannot run getSticker happy path.", + ); + } + const stickerId = firstPack.stickers[0]!.id; + const result = await runEffect(getSticker({ sticker_id: stickerId })); + expect(result).not.toBeNull(); + expect(typeof result).toBe("object"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sticker = result as any; + expect(sticker.id).toBe(stickerId); + expect(typeof sticker.name).toBe("string"); + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent sticker id", async () => { + await runEffect( + getSticker({ sticker_id: NON_EXISTENT_STICKER_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing sticker as NotFound. Some malformed or + // out-of-range snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a sticker the bot cannot access", async () => { + await runEffect( + getSticker({ sticker_id: INACCESSIBLE_STICKER_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // A guild sticker the bot is not in typically surfaces as Forbidden, + // but Discord often returns NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getStickerPack.test.ts b/packages/discord/test/getStickerPack.test.ts new file mode 100644 index 000000000..00039da69 --- /dev/null +++ b/packages/discord/test/getStickerPack.test.ts @@ -0,0 +1,92 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getStickerPack } from "../src/operations/getStickerPack.ts"; +import { listStickerPacks } from "../src/operations/listStickerPacks.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /sticker-packs/{pack_id} fetches a single sticker pack. Sticker packs +// are public Discord-curated content, so the happy path resolves a real +// pack id by listing sticker packs and reading the first one. + +// Snowflake-shaped ids unlikely to resolve to any real sticker pack. +const NON_EXISTENT_PACK_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_PACK_ID = "100000000000000001"; + +describe("getStickerPack", () => { + it( + "happy path - fetches a sticker pack", + async () => { + const packs = await runEffect(listStickerPacks({})); + const firstPack = packs.sticker_packs[0]; + if (!firstPack) { + throw new Error( + "No sticker packs were returned by listStickerPacks; cannot run getStickerPack happy path.", + ); + } + const result = await runEffect(getStickerPack({ pack_id: firstPack.id })); + expect(result.id).toBe(firstPack.id); + expect(typeof result.name).toBe("string"); + expect(typeof result.sku_id).toBe("string"); + expect( + result.description === null || typeof result.description === "string", + ).toBe(true); + expect(Array.isArray(result.stickers)).toBe(true); + for (const s of result.stickers) { + expect(typeof s.id).toBe("string"); + expect(typeof s.name).toBe("string"); + expect(s.pack_id).toBe(firstPack.id); + expect(typeof s.sort_value).toBe("number"); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent sticker pack id", async () => { + await runEffect( + getStickerPack({ pack_id: NON_EXISTENT_PACK_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing sticker pack as NotFound. Some + // malformed or out-of-range snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a sticker pack the bot cannot access", async () => { + await runEffect( + getStickerPack({ pack_id: INACCESSIBLE_PACK_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // A pack id the bot is not entitled to typically surfaces as + // Forbidden, but Discord often returns NotFound to avoid leaking + // existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getThreadMember.test.ts b/packages/discord/test/getThreadMember.test.ts new file mode 100644 index 000000000..137874c7b --- /dev/null +++ b/packages/discord/test/getThreadMember.test.ts @@ -0,0 +1,116 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { addThreadMember } from "../src/operations/addThreadMember.ts"; +import { deleteThreadMember } from "../src/operations/deleteThreadMember.ts"; +import { getThreadMember } from "../src/operations/getThreadMember.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// A real Discord thread (channel) the bot can read, and a real user_id the +// bot can add to that thread. The bot must already be a member of the +// thread (or have permission to manage it). +const TEST_THREAD_ID = process.env.DISCORD_TEST_THREAD_ID; +const TEST_USER_ID = process.env.DISCORD_TEST_USER_ID; + +// Snowflake-format identifiers that should not match real Discord resources. +const NON_EXISTENT_THREAD_ID = "100000000000000000"; +const NON_EXISTENT_USER_ID = "100000000000000001"; + +describe("getThreadMember", () => { + it( + "happy path - fetches a thread member by user id", + async () => { + if (!TEST_THREAD_ID || !TEST_USER_ID) { + throw new Error( + "DISCORD_TEST_THREAD_ID and DISCORD_TEST_USER_ID env vars are required for the getThreadMember happy path", + ); + } + await runEffect( + Effect.gen(function* () { + // Ensure the user is a member of the thread for this test run. + yield* addThreadMember({ + channel_id: TEST_THREAD_ID, + user_id: TEST_USER_ID, + }); + const result = yield* getThreadMember({ + channel_id: TEST_THREAD_ID, + user_id: TEST_USER_ID, + with_member: true, + }); + expect(typeof result.id).toBe("string"); + expect(result.user_id).toBe(TEST_USER_ID); + expect(typeof result.join_timestamp).toBe("string"); + expect(typeof result.flags).toBe("number"); + if (result.member !== undefined) { + expect(typeof result.member.flags).toBe("number"); + expect(typeof result.member.joined_at).toBe("string"); + expect(Array.isArray(result.member.roles)).toBe(true); + expect(result.member.user.id).toBe(TEST_USER_ID); + expect(typeof result.member.user.username).toBe("string"); + } + }).pipe( + Effect.ensuring( + deleteThreadMember({ + channel_id: TEST_THREAD_ID, + user_id: TEST_USER_ID, + }).pipe(Effect.ignore), + ), + ), + ); + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for non-existent thread channel_id", async () => { + await runEffect( + getThreadMember({ + channel_id: NON_EXISTENT_THREAD_ID, + user_id: TEST_USER_ID ?? NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for an unseen thread but may surface as + // Forbidden when the bot can't see the channel, or BadRequest if + // the snowflake is otherwise rejected. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a thread the bot has no access to", async () => { + await runEffect( + getThreadMember({ + channel_id: NON_EXISTENT_THREAD_ID, + user_id: NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // For a thread the bot is not in, Discord typically returns + // Forbidden (50001 Missing Access) but often returns NotFound to + // avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getUser.test.ts b/packages/discord/test/getUser.test.ts new file mode 100644 index 000000000..c78ff1f4b --- /dev/null +++ b/packages/discord/test/getUser.test.ts @@ -0,0 +1,87 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { getMyUser } from "../src/operations/getMyUser.ts"; +import { getUser } from "../src/operations/getUser.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /users/{user_id} fetches any user visible to the bot. The happy path +// resolves the bot's own user id via /users/@me, which is always reachable +// with a Bot token, then fetches the same user via /users/{user_id}. + +// Snowflake-shaped ids unlikely to resolve to any real user. +const NON_EXISTENT_USER_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_USER_ID = "100000000000000001"; + +describe("getUser", () => { + it( + "happy path - fetches a user by id", + async () => { + const me = await runEffect(getMyUser({})); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const myId = (me as any).id as string; + expect(typeof myId).toBe("string"); + + const result = await runEffect(getUser({ user_id: myId })); + expect(result.id).toBe(myId); + expect(typeof result.username).toBe("string"); + expect(typeof result.discriminator).toBe("string"); + expect(typeof result.public_flags).toBe("number"); + expect(typeof result.flags).toBe("number"); + expect(result.avatar === null || typeof result.avatar === "string").toBe( + true, + ); + expect( + result.global_name === null || typeof result.global_name === "string", + ).toBe(true); + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent user id", async () => { + await runEffect( + getUser({ user_id: NON_EXISTENT_USER_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing user as NotFound. Some malformed or + // out-of-range snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a user the bot cannot access", async () => { + await runEffect( + getUser({ user_id: INACCESSIBLE_USER_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // A user id the bot cannot resolve typically surfaces as Forbidden, + // but Discord often returns NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getVoiceState.test.ts b/packages/discord/test/getVoiceState.test.ts new file mode 100644 index 000000000..d5fe903cf --- /dev/null +++ b/packages/discord/test/getVoiceState.test.ts @@ -0,0 +1,110 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { getVoiceState } from "../src/operations/getVoiceState.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/voice-states/{user_id} requires that the target +// user is currently connected to a voice channel in the guild and that the +// bot has permission to read voice states there. This cannot be cleanly +// created via the API in a test fixture, so the happy path is gated on +// operator-supplied env vars pointing at a guild and a user known to be +// connected to voice. +const TEST_VOICE_GUILD_ID = process.env.DISCORD_TEST_VOICE_GUILD_ID; +const TEST_VOICE_USER_ID = process.env.DISCORD_TEST_VOICE_USER_ID; + +// Snowflake-shaped ids unlikely to resolve to any real guild/user. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const NON_EXISTENT_USER_ID = "100000000000000001"; +const INACCESSIBLE_GUILD_ID = "100000000000000002"; + +describe("getVoiceState", () => { + it( + "happy path - fetches a voice state for a connected user", + async () => { + if (!TEST_VOICE_GUILD_ID || !TEST_VOICE_USER_ID) { + throw new Error( + "DISCORD_TEST_VOICE_GUILD_ID and DISCORD_TEST_VOICE_USER_ID env vars are required for the getVoiceState happy path. " + + "Set them to a guild id and a user id known to be connected to a voice channel in that guild.", + ); + } + const result = await runEffect( + getVoiceState({ + guild_id: TEST_VOICE_GUILD_ID, + user_id: TEST_VOICE_USER_ID, + }), + ); + expect(result.user_id).toBe(TEST_VOICE_USER_ID); + expect(typeof result.session_id).toBe("string"); + expect(typeof result.deaf).toBe("boolean"); + expect(typeof result.mute).toBe("boolean"); + expect(typeof result.self_deaf).toBe("boolean"); + expect(typeof result.self_mute).toBe("boolean"); + expect(typeof result.self_video).toBe("boolean"); + expect(typeof result.suppress).toBe("boolean"); + expect( + result.self_stream === null || typeof result.self_stream === "boolean", + ).toBe(true); + expect( + result.request_to_speak_timestamp === null || + typeof result.request_to_speak_timestamp === "string", + ).toBe(true); + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent guild_id", async () => { + await runEffect( + getVoiceState({ + guild_id: NON_EXISTENT_GUILD_ID, + user_id: TEST_VOICE_USER_ID ?? NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing guild as NotFound. The bot may also see + // it as Forbidden when it has no access, or BadRequest if the + // snowflake is otherwise rejected. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot cannot access", async () => { + await runEffect( + getVoiceState({ + guild_id: INACCESSIBLE_GUILD_ID, + user_id: NON_EXISTENT_USER_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // For a guild the bot is not in, Discord typically returns + // Forbidden (50001 Missing Access) but often returns NotFound to + // avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getWebhook.test.ts b/packages/discord/test/getWebhook.test.ts new file mode 100644 index 000000000..d40c1d925 --- /dev/null +++ b/packages/discord/test/getWebhook.test.ts @@ -0,0 +1,99 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { createWebhook } from "../src/operations/createWebhook.ts"; +import { deleteWebhook } from "../src/operations/deleteWebhook.ts"; +import { getWebhook } from "../src/operations/getWebhook.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /webhooks/{webhook_id} fetches a webhook by id. The happy path creates +// a webhook in an operator-supplied text channel (DISCORD_TEST_CHANNEL_ID), +// fetches it, and deletes it on cleanup. The bot must have MANAGE_WEBHOOKS +// in that channel. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-shaped ids unlikely to resolve to any real webhook. +const NON_EXISTENT_WEBHOOK_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_WEBHOOK_ID = "100000000000000001"; + +describe("getWebhook", () => { + it( + "happy path - fetches a webhook by id", + async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the getWebhook happy path. " + + "Set it to a text channel id where the bot has MANAGE_WEBHOOKS.", + ); + } + const webhookName = `distilled-discord-webhook-${testRunId}`; + const created = await runEffect( + createWebhook({ channel_id: TEST_CHANNEL_ID, name: webhookName }), + ); + const webhookId = created.id; + try { + const result = await runEffect(getWebhook({ webhook_id: webhookId })); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const webhook = result as any; + expect(webhook.id).toBe(webhookId); + expect(webhook.name).toBe(webhookName); + expect( + webhook.avatar === null || typeof webhook.avatar === "string", + ).toBe(true); + } finally { + await runEffect( + deleteWebhook({ webhook_id: webhookId }).pipe(Effect.ignore), + ); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent webhook id", async () => { + await runEffect( + getWebhook({ webhook_id: NON_EXISTENT_WEBHOOK_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing webhook as NotFound. Some malformed or + // out-of-range snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a webhook the bot cannot access", async () => { + await runEffect( + getWebhook({ webhook_id: INACCESSIBLE_WEBHOOK_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // A webhook in a channel the bot cannot see typically surfaces as + // Forbidden, but Discord often returns NotFound to avoid leaking + // existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getWebhookByToken.test.ts b/packages/discord/test/getWebhookByToken.test.ts new file mode 100644 index 000000000..98dac19d9 --- /dev/null +++ b/packages/discord/test/getWebhookByToken.test.ts @@ -0,0 +1,126 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { createWebhook } from "../src/operations/createWebhook.ts"; +import { deleteWebhook } from "../src/operations/deleteWebhook.ts"; +import { getWebhookByToken } from "../src/operations/getWebhookByToken.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /webhooks/{webhook_id}/{webhook_token} fetches a webhook using its +// token; this route does not require bot auth (the token in the URL is the +// credential). The happy path creates a webhook in an operator-supplied +// text channel (DISCORD_TEST_CHANNEL_ID), reads its returned token, fetches +// it via the token route, and deletes the webhook on cleanup. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-shaped ids unlikely to resolve to any real webhook, plus a +// plausible-looking but invalid token. +const NON_EXISTENT_WEBHOOK_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_WEBHOOK_ID = "100000000000000001"; +const BOGUS_WEBHOOK_TOKEN = `distilled-bogus-token-${testRunId}`; + +describe("getWebhookByToken", () => { + it( + "happy path - fetches a webhook by id and token", + async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the getWebhookByToken happy path. " + + "Set it to a text channel id where the bot has MANAGE_WEBHOOKS.", + ); + } + const webhookName = `distilled-discord-webhook-${testRunId}`; + const created = await runEffect( + createWebhook({ channel_id: TEST_CHANNEL_ID, name: webhookName }), + ); + const webhookId = created.id; + const webhookToken = created.token; + if (!webhookToken) { + // Defensive: incoming webhooks always include a token, but if the + // server omits it we cannot exercise the token route — fail loudly + // so the operator can investigate, then clean up. + await runEffect( + deleteWebhook({ webhook_id: webhookId }).pipe(Effect.ignore), + ); + throw new Error( + "createWebhook did not return a token; cannot exercise getWebhookByToken happy path.", + ); + } + try { + const result = await runEffect( + getWebhookByToken({ + webhook_id: webhookId, + webhook_token: webhookToken, + }), + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const webhook = result as any; + expect(webhook.id).toBe(webhookId); + expect(webhook.name).toBe(webhookName); + expect(typeof webhook.token).toBe("string"); + expect( + webhook.avatar === null || typeof webhook.avatar === "string", + ).toBe(true); + } finally { + await runEffect( + deleteWebhook({ webhook_id: webhookId }).pipe(Effect.ignore), + ); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent webhook id", async () => { + await runEffect( + getWebhookByToken({ + webhook_id: NON_EXISTENT_WEBHOOK_ID, + webhook_token: BOGUS_WEBHOOK_TOKEN, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing webhook as NotFound. Some malformed or + // out-of-range snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a webhook the caller cannot access", async () => { + await runEffect( + getWebhookByToken({ + webhook_id: INACCESSIBLE_WEBHOOK_ID, + webhook_token: BOGUS_WEBHOOK_TOKEN, + }).pipe( + Effect.flip, + Effect.map((e) => { + // A webhook id paired with a bogus token typically surfaces as + // Forbidden (invalid webhook token), but Discord often returns + // NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/getWebhookMessage.test.ts b/packages/discord/test/getWebhookMessage.test.ts new file mode 100644 index 000000000..fecccdce2 --- /dev/null +++ b/packages/discord/test/getWebhookMessage.test.ts @@ -0,0 +1,171 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { getWebhookMessage } from "../src/operations/getWebhookMessage.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /webhooks/{webhook_id}/{webhook_token}/messages/{message_id} — fetches +// a specific message previously sent by the webhook. The webhook path is +// unauthenticated (the token in the path is the credential); the webhook_id +// must point at a real webhook, the token must match, and the message_id +// must reference a message posted by that webhook. +// +// The happy path requires operator-supplied env vars pointing at a webhook +// and a known message id posted via that webhook. +const TEST_WEBHOOK_ID = process.env.DISCORD_TEST_WEBHOOK_ID; +const TEST_WEBHOOK_TOKEN = process.env.DISCORD_TEST_WEBHOOK_TOKEN; +const TEST_WEBHOOK_MESSAGE_ID = process.env.DISCORD_TEST_WEBHOOK_MESSAGE_ID; + +// Snowflake-shaped ids and a randomly-generated token unlikely to match any +// real webhook. Discord typically returns 404 (10015 — Unknown Webhook) for +// missing ids, and 401/403 for token mismatches. +const NON_EXISTENT_WEBHOOK_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const NON_EXISTENT_WEBHOOK_TOKEN = `distilled-bogus-webhook-token-${testRunId}`; +const NON_EXISTENT_MESSAGE_ID = "100000000000000002"; + +describe("getWebhookMessage", () => { + it( + "happy path - fetches a webhook message by id", + async () => { + if (!TEST_WEBHOOK_ID || !TEST_WEBHOOK_TOKEN || !TEST_WEBHOOK_MESSAGE_ID) { + throw new Error( + "DISCORD_TEST_WEBHOOK_ID, DISCORD_TEST_WEBHOOK_TOKEN, and DISCORD_TEST_WEBHOOK_MESSAGE_ID must be set " + + "for the getWebhookMessage happy path. The message_id must reference a message posted via the webhook token.", + ); + } + const result = await runEffect( + getWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: TEST_WEBHOOK_TOKEN, + message_id: TEST_WEBHOOK_MESSAGE_ID, + }), + ); + expect(result.id).toBe(TEST_WEBHOOK_MESSAGE_ID); + expect(typeof result.channel_id).toBe("string"); + expect(typeof result.content).toBe("string"); + expect(typeof result.timestamp).toBe("string"); + expect( + result.edited_timestamp === null || + typeof result.edited_timestamp === "string", + ).toBe(true); + expect(typeof result.flags).toBe("number"); + expect(Array.isArray(result.mentions)).toBe(true); + expect(Array.isArray(result.mention_roles)).toBe(true); + expect(Array.isArray(result.attachments)).toBe(true); + expect(Array.isArray(result.embeds)).toBe(true); + expect(Array.isArray(result.components)).toBe(true); + expect(typeof result.author.id).toBe("string"); + expect(typeof result.author.username).toBe("string"); + expect(typeof result.pinned).toBe("boolean"); + expect(typeof result.mention_everyone).toBe("boolean"); + expect(typeof result.tts).toBe("boolean"); + }, + { timeout: 30_000 }, + ); + + it( + "happy path - accepts the optional thread_id query parameter", + async () => { + if (!TEST_WEBHOOK_ID || !TEST_WEBHOOK_TOKEN || !TEST_WEBHOOK_MESSAGE_ID) { + throw new Error( + "DISCORD_TEST_WEBHOOK_ID, DISCORD_TEST_WEBHOOK_TOKEN, and DISCORD_TEST_WEBHOOK_MESSAGE_ID must be set " + + "for the getWebhookMessage happy path.", + ); + } + // Optional thread_id — if the operator supplies a thread id where the + // webhook posted the message, use it; otherwise skip silently because + // re-running the previous case would be redundant. + const threadId = process.env.DISCORD_TEST_WEBHOOK_THREAD_ID; + if (!threadId) return; + const result = await runEffect( + getWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: TEST_WEBHOOK_TOKEN, + message_id: TEST_WEBHOOK_MESSAGE_ID, + thread_id: threadId, + }), + ); + expect(result.id).toBe(TEST_WEBHOOK_MESSAGE_ID); + expect(typeof result.content).toBe("string"); + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent webhook id", async () => { + await runEffect( + getWebhookMessage({ + webhook_id: NON_EXISTENT_WEBHOOK_ID, + webhook_token: NON_EXISTENT_WEBHOOK_TOKEN, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord typically returns NotFound (10015 — Unknown Webhook) for + // missing webhook ids. A token mismatch on a real webhook id may + // surface as Forbidden, and malformed snowflakes as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a token mismatch", async () => { + if (!TEST_WEBHOOK_ID) { + // Without a real webhook id, fall back to the missing-id case. + await runEffect( + getWebhookMessage({ + webhook_id: "100000000000000001", + webhook_token: NON_EXISTENT_WEBHOOK_TOKEN, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + return; + } + // Real webhook id + bogus token typically yields 401 or 403; some + // routes resolve as 404 instead. + await runEffect( + getWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: NON_EXISTENT_WEBHOOK_TOKEN, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "NotFound", + "Forbidden", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/guildRoleMemberCounts.test.ts b/packages/discord/test/guildRoleMemberCounts.test.ts new file mode 100644 index 000000000..630a954e7 --- /dev/null +++ b/packages/discord/test/guildRoleMemberCounts.test.ts @@ -0,0 +1,87 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { guildRoleMemberCounts } from "../src/operations/guildRoleMemberCounts.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/roles/member-counts returns a record of role_id -> +// member count for the guild. The bot must be a member of the guild. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real guild. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("guildRoleMemberCounts", () => { + it( + "happy path - returns role member counts for a guild", + async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the guildRoleMemberCounts happy path. " + + "Set it to a guild id where the bot is a member.", + ); + } + const result = await runEffect( + guildRoleMemberCounts({ guild_id: TEST_GUILD_ID }), + ); + expect(result).not.toBeNull(); + expect(typeof result).toBe("object"); + for (const [roleId, count] of Object.entries(result)) { + expect(typeof roleId).toBe("string"); + expect(typeof count).toBe("number"); + expect(count).toBeGreaterThanOrEqual(0); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent guild_id", async () => { + await runEffect( + guildRoleMemberCounts({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing guild as NotFound. The bot may also see + // it as Forbidden when it has no access, or BadRequest if the + // snowflake is otherwise rejected. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot cannot access", async () => { + await runEffect( + guildRoleMemberCounts({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // For a guild the bot is not in, Discord typically returns + // Forbidden (50001 Missing Access) but often returns NotFound to + // avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/guildSearch.test.ts b/packages/discord/test/guildSearch.test.ts new file mode 100644 index 000000000..f6ca1f7f1 --- /dev/null +++ b/packages/discord/test/guildSearch.test.ts @@ -0,0 +1,108 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { guildSearch } from "../src/operations/guildSearch.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/messages/search runs the guild's message search +// index. Historically this route is restricted to user accounts and is +// rejected for bot tokens, so the happy path is gated on +// DISCORD_TEST_GUILD_ID and may require user-token credentials to pass. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real guild. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("guildSearch", () => { + it( + "happy path - searches a guild's messages", + async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the guildSearch happy path. " + + "Note that Discord typically restricts /guilds/{guild_id}/messages/search to user accounts.", + ); + } + const result = await runEffect( + guildSearch({ + guild_id: TEST_GUILD_ID, + content: `distilled-test-${testRunId}`, + limit: 1, + }), + ); + expect(Array.isArray(result.messages)).toBe(true); + expect(typeof result.total_results).toBe("number"); + expect(result.total_results).toBeGreaterThanOrEqual(0); + expect(typeof result.doing_deep_historical_index).toBe("boolean"); + for (const group of result.messages) { + expect(Array.isArray(group)).toBe(true); + for (const m of group) { + expect(typeof m.id).toBe("string"); + expect(typeof m.channel_id).toBe("string"); + expect(typeof m.content).toBe("string"); + expect(typeof m.timestamp).toBe("string"); + expect(typeof m.hit).toBe("boolean"); + } + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent guild_id", async () => { + await runEffect( + guildSearch({ + guild_id: NON_EXISTENT_GUILD_ID, + content: `distilled-test-${testRunId}`, + limit: 1, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing guild as NotFound. Some routes resolve + // to Forbidden (notably for bot tokens on this user-only endpoint), + // and malformed snowflakes as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the caller cannot access", async () => { + await runEffect( + guildSearch({ + guild_id: INACCESSIBLE_GUILD_ID, + content: `distilled-test-${testRunId}`, + limit: 1, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Bots calling this user-only endpoint typically receive Forbidden; + // a guild the caller is not in often returns NotFound to avoid + // leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/inviteResolve.test.ts b/packages/discord/test/inviteResolve.test.ts new file mode 100644 index 000000000..48001d027 --- /dev/null +++ b/packages/discord/test/inviteResolve.test.ts @@ -0,0 +1,100 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { createChannelInvite } from "../src/operations/createChannelInvite.ts"; +import { inviteResolve } from "../src/operations/inviteResolve.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /invites/{code} resolves an invite. The happy path creates a fresh +// channel invite in an operator-supplied text channel (DISCORD_TEST_CHANNEL_ID) +// and resolves its code. Discord auto-expires invites after 24h by default, +// and there is no programmatic delete in this SDK, so the created invite +// simply ages out. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Invite codes are short alphanumeric strings. A random one is overwhelmingly +// unlikely to resolve to a real invite. +const NON_EXISTENT_INVITE_CODE = `distilled-${testRunId}`; +const INACCESSIBLE_INVITE_CODE = `inv-${testRunId}-x`; + +describe("inviteResolve", () => { + it( + "happy path - resolves an invite code", + async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the inviteResolve happy path. " + + "Set it to a text channel id where the bot has CREATE_INSTANT_INVITE.", + ); + } + const created = await runEffect( + createChannelInvite({ channel_id: TEST_CHANNEL_ID }), + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const code = (created as any).code as string; + expect(typeof code).toBe("string"); + + const result = await runEffect( + inviteResolve({ code, with_counts: true }), + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const invite = result as any; + expect(invite.code).toBe(code); + // The invite must reference some channel; usually `channel.id` matches + // TEST_CHANNEL_ID, though Discord may surface group invites differently. + if (invite.channel && typeof invite.channel === "object") { + expect(typeof invite.channel.id).toBe("string"); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent invite code", async () => { + await runEffect( + inviteResolve({ code: NON_EXISTENT_INVITE_CODE }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing invite as NotFound (10006 — Unknown + // Invite). Some malformed codes may surface as BadRequest or be + // forbidden depending on routing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for an invite the caller cannot resolve", async () => { + await runEffect( + inviteResolve({ code: INACCESSIBLE_INVITE_CODE }).pipe( + Effect.flip, + Effect.map((e) => { + // An invite the caller cannot resolve typically surfaces as + // Forbidden, but Discord often returns NotFound to avoid leaking + // existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/inviteRevoke.test.ts b/packages/discord/test/inviteRevoke.test.ts new file mode 100644 index 000000000..09e59e1d7 --- /dev/null +++ b/packages/discord/test/inviteRevoke.test.ts @@ -0,0 +1,93 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { createChannelInvite } from "../src/operations/createChannelInvite.ts"; +import { inviteRevoke } from "../src/operations/inviteRevoke.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// DELETE /invites/{code} revokes an invite. The happy path creates a fresh +// channel invite in an operator-supplied text channel (DISCORD_TEST_CHANNEL_ID) +// and revokes it. The bot must have MANAGE_CHANNELS in the channel or +// MANAGE_GUILD on the guild. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Invite codes are short alphanumeric strings. A random one is overwhelmingly +// unlikely to resolve to a real invite. +const NON_EXISTENT_INVITE_CODE = `distilled-${testRunId}`; +const INACCESSIBLE_INVITE_CODE = `inv-${testRunId}-x`; + +describe("inviteRevoke", () => { + it( + "happy path - revokes an invite", + async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the inviteRevoke happy path. " + + "Set it to a text channel id where the bot has MANAGE_CHANNELS.", + ); + } + const created = await runEffect( + createChannelInvite({ channel_id: TEST_CHANNEL_ID }), + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const code = (created as any).code as string; + expect(typeof code).toBe("string"); + + const result = await runEffect(inviteRevoke({ code })); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const revoked = result as any; + // Discord returns the deleted invite object on success. + expect(revoked.code).toBe(code); + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent invite code", async () => { + await runEffect( + inviteRevoke({ code: NON_EXISTENT_INVITE_CODE }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing invite as NotFound (10006 — Unknown + // Invite). Some malformed codes may surface as BadRequest or be + // forbidden depending on routing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for an invite the caller cannot revoke", async () => { + await runEffect( + inviteRevoke({ code: INACCESSIBLE_INVITE_CODE }).pipe( + Effect.flip, + Effect.map((e) => { + // An invite the caller cannot revoke typically surfaces as + // Forbidden, but Discord often returns NotFound to avoid leaking + // existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/joinThread.test.ts b/packages/discord/test/joinThread.test.ts new file mode 100644 index 000000000..cdbc40168 --- /dev/null +++ b/packages/discord/test/joinThread.test.ts @@ -0,0 +1,112 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { joinThread } from "../src/operations/joinThread.ts"; +import { leaveThread } from "../src/operations/leaveThread.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PUT /channels/{channel_id}/thread-members/@me — adds the current user +// (the bot) to the thread. The bot must be able to see the thread. +const TEST_THREAD_ID = process.env.DISCORD_TEST_THREAD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real thread. +const NON_EXISTENT_THREAD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_THREAD_ID = "100000000000000001"; +// A snowflake that is likely valid-looking but malformed in subtle ways +// triggers BadRequest on some routes. +const MALFORMED_THREAD_ID = "not-a-snowflake"; + +describe("joinThread", () => { + it( + "happy path - bot joins a thread and leaves it on cleanup", + async () => { + if (!TEST_THREAD_ID) { + throw new Error( + "DISCORD_TEST_THREAD_ID env var is required for the joinThread happy path. " + + "Set it to a thread (channel) the bot can see.", + ); + } + await runEffect( + joinThread({ channel_id: TEST_THREAD_ID }).pipe( + Effect.tap((result) => + Effect.sync(() => { + // Discord returns 204 No Content on success — the operation + // succeeded if no error was thrown. + expect(result).toBeUndefined(); + }), + ), + Effect.ensuring( + leaveThread({ channel_id: TEST_THREAD_ID }).pipe(Effect.ignore), + ), + ), + ); + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent thread channel_id", async () => { + await runEffect( + joinThread({ channel_id: NON_EXISTENT_THREAD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for an unseen thread but may surface as + // Forbidden when the bot can't see the channel, or BadRequest if + // the snowflake is otherwise rejected. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a thread the bot has no access to", async () => { + await runEffect( + joinThread({ channel_id: INACCESSIBLE_THREAD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // For a thread the bot cannot see, Discord typically returns + // Forbidden (50001 Missing Access) but often returns NotFound to + // avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - BadRequest for a malformed (non-snowflake) channel_id", async () => { + await runEffect( + joinThread({ channel_id: MALFORMED_THREAD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord rejects malformed snowflakes with 400 Invalid Form Body; + // routing layers may also classify it as 404. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/leaveGuild.test.ts b/packages/discord/test/leaveGuild.test.ts new file mode 100644 index 000000000..db511b5aa --- /dev/null +++ b/packages/discord/test/leaveGuild.test.ts @@ -0,0 +1,89 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { leaveGuild } from "../src/operations/leaveGuild.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// DELETE /users/@me/guilds/{guild_id} — the current user (bot) leaves the +// guild. This is a one-way destructive operation: after success the bot is +// no longer a member of the guild and must be re-invited to run the happy +// path again. The operator must set DISCORD_TEST_LEAVE_GUILD_ID to a +// throwaway guild the bot has been added to specifically for this test. +// +// A bot cannot leave a guild it owns; for owned guilds Discord returns +// BadRequest. The operator-supplied guild must therefore be one the bot +// does not own. +const TEST_LEAVE_GUILD_ID = process.env.DISCORD_TEST_LEAVE_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real guild membership. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("leaveGuild", () => { + it( + "happy path - the bot leaves a guild", + async () => { + if (!TEST_LEAVE_GUILD_ID) { + throw new Error( + "DISCORD_TEST_LEAVE_GUILD_ID env var is required for the leaveGuild happy path. " + + "Set it to a throwaway guild id the bot has been invited to. The bot will be removed " + + "from this guild on success and must be re-invited before re-running.", + ); + } + const result = await runEffect( + leaveGuild({ guild_id: TEST_LEAVE_GUILD_ID }), + ); + // Discord returns 204 No Content on success. + expect(result).toBeUndefined(); + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a guild the bot is not a member of", async () => { + await runEffect( + leaveGuild({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing membership as NotFound (10004 — Unknown + // Guild). Some malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the caller cannot leave", async () => { + await runEffect( + leaveGuild({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // A guild id the bot is not a member of typically surfaces as + // NotFound, but Discord can also classify ownership conflicts as + // Forbidden or BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/leaveLobby.test.ts b/packages/discord/test/leaveLobby.test.ts new file mode 100644 index 000000000..420a13b00 --- /dev/null +++ b/packages/discord/test/leaveLobby.test.ts @@ -0,0 +1,94 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { createLobby } from "../src/operations/createLobby.ts"; +import { getMyUser } from "../src/operations/getMyUser.ts"; +import { leaveLobby } from "../src/operations/leaveLobby.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// DELETE /lobbies/{lobby_id}/members/@me — the current user (bot) leaves a +// lobby. The happy path resolves the bot's own user id via /users/@me, +// creates a short-idle lobby with the bot listed as a member, and then +// calls leaveLobby. Lobbies auto-expire via `idle_timeout_seconds` after +// the last member departs. +// +// The application must have the LOBBIES_WRITE scope to create lobbies. + +// Snowflake-shaped ids unlikely to resolve to any real lobby. +const NON_EXISTENT_LOBBY_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_LOBBY_ID = "100000000000000001"; + +describe("leaveLobby", () => { + it( + "happy path - the bot leaves a lobby it joined", + async () => { + const me = await runEffect(getMyUser({})); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const myId = (me as any).id as string; + expect(typeof myId).toBe("string"); + + const lobby = await runEffect( + createLobby({ + idle_timeout_seconds: 5, + members: [{ id: myId, metadata: { distilled_test: testRunId } }], + metadata: { distilled_test: testRunId }, + }), + ); + const lobbyId = lobby.id; + + const result = await runEffect(leaveLobby({ lobby_id: lobbyId })); + // Discord returns 204 No Content on success. + expect(result).toBeUndefined(); + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent lobby id", async () => { + await runEffect( + leaveLobby({ lobby_id: NON_EXISTENT_LOBBY_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing lobby as NotFound. The application + // may also see it as Forbidden when it does not own the lobby, + // and malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a lobby the application cannot leave", async () => { + await runEffect( + leaveLobby({ lobby_id: INACCESSIBLE_LOBBY_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // A lobby the bot is not a member of typically surfaces as + // Forbidden, but Discord often returns NotFound to avoid leaking + // existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/leaveThread.test.ts b/packages/discord/test/leaveThread.test.ts new file mode 100644 index 000000000..9f2c6b1cc --- /dev/null +++ b/packages/discord/test/leaveThread.test.ts @@ -0,0 +1,86 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { joinThread } from "../src/operations/joinThread.ts"; +import { leaveThread } from "../src/operations/leaveThread.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// DELETE /channels/{channel_id}/thread-members/@me — removes the current +// user (the bot) from the thread. The setup adds the bot via joinThread, +// then the happy path leaves it. +const TEST_THREAD_ID = process.env.DISCORD_TEST_THREAD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real thread. +const NON_EXISTENT_THREAD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_THREAD_ID = "100000000000000001"; + +describe("leaveThread", () => { + it( + "happy path - bot leaves a thread it joined", + async () => { + if (!TEST_THREAD_ID) { + throw new Error( + "DISCORD_TEST_THREAD_ID env var is required for the leaveThread happy path. " + + "Set it to a thread (channel) the bot can see.", + ); + } + // Join first so leave has something to do. + await runEffect(joinThread({ channel_id: TEST_THREAD_ID })); + const result = await runEffect( + leaveThread({ channel_id: TEST_THREAD_ID }), + ); + // Discord returns 204 No Content on success. + expect(result).toBeUndefined(); + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent thread channel_id", async () => { + await runEffect( + leaveThread({ channel_id: NON_EXISTENT_THREAD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns NotFound for an unseen thread but may surface as + // Forbidden when the bot can't see the channel, or BadRequest if + // the snowflake is otherwise rejected. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a thread the bot has no access to", async () => { + await runEffect( + leaveThread({ channel_id: INACCESSIBLE_THREAD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // For a thread the bot cannot see, Discord typically returns + // Forbidden (50001 Missing Access) but often returns NotFound to + // avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listApplicationCommands.test.ts b/packages/discord/test/listApplicationCommands.test.ts new file mode 100644 index 000000000..46f5cf89d --- /dev/null +++ b/packages/discord/test/listApplicationCommands.test.ts @@ -0,0 +1,100 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { getMyApplication } from "../src/operations/getMyApplication.ts"; +import { listApplicationCommands } from "../src/operations/listApplicationCommands.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /applications/{application_id}/commands lists global application +// commands for an application. The happy path resolves the bot's own +// application id via /applications/@me, then lists its commands. The +// command list is allowed to be empty. + +// Snowflake-shaped ids unlikely to resolve to any real application. +const NON_EXISTENT_APPLICATION_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_APPLICATION_ID = "100000000000000001"; + +describe("listApplicationCommands", () => { + it( + "happy path - lists application commands for the bot's application", + async () => { + const app = await runEffect(getMyApplication({})); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const applicationId = (app as any).id as string; + expect(typeof applicationId).toBe("string"); + + const result = await runEffect( + listApplicationCommands({ + application_id: applicationId, + with_localizations: true, + }), + ); + expect(Array.isArray(result)).toBe(true); + for (const cmd of result) { + expect(typeof cmd.id).toBe("string"); + expect(cmd.application_id).toBe(applicationId); + expect(typeof cmd.version).toBe("string"); + expect(typeof cmd.name).toBe("string"); + expect(typeof cmd.description).toBe("string"); + expect( + cmd.default_member_permissions === null || + typeof cmd.default_member_permissions === "string", + ).toBe(true); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent application_id", async () => { + await runEffect( + listApplicationCommands({ + application_id: NON_EXISTENT_APPLICATION_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing application as NotFound. Bot tokens + // calling for a different application typically receive Forbidden, + // and malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for an application the bot does not own", async () => { + await runEffect( + listApplicationCommands({ + application_id: INACCESSIBLE_APPLICATION_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list commands for its own application; for any + // other application Discord returns Forbidden, but it often returns + // NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listApplicationEmojis.test.ts b/packages/discord/test/listApplicationEmojis.test.ts new file mode 100644 index 000000000..4f5138151 --- /dev/null +++ b/packages/discord/test/listApplicationEmojis.test.ts @@ -0,0 +1,96 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { getMyApplication } from "../src/operations/getMyApplication.ts"; +import { listApplicationEmojis } from "../src/operations/listApplicationEmojis.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /applications/{application_id}/emojis lists the application-owned +// emojis for an application. The happy path resolves the bot's own +// application id via /applications/@me, then lists its emojis. The emoji +// list is allowed to be empty. + +// Snowflake-shaped ids unlikely to resolve to any real application. +const NON_EXISTENT_APPLICATION_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_APPLICATION_ID = "100000000000000001"; + +describe("listApplicationEmojis", () => { + it( + "happy path - lists emojis for the bot's application", + async () => { + const app = await runEffect(getMyApplication({})); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const applicationId = (app as any).id as string; + expect(typeof applicationId).toBe("string"); + + const result = await runEffect( + listApplicationEmojis({ application_id: applicationId }), + ); + expect(result).toBeDefined(); + expect(Array.isArray(result.items)).toBe(true); + for (const emoji of result.items) { + expect(typeof emoji.id).toBe("string"); + expect(typeof emoji.name).toBe("string"); + expect(Array.isArray(emoji.roles)).toBe(true); + expect(typeof emoji.require_colons).toBe("boolean"); + expect(typeof emoji.managed).toBe("boolean"); + expect(typeof emoji.animated).toBe("boolean"); + expect(typeof emoji.available).toBe("boolean"); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent application_id", async () => { + await runEffect( + listApplicationEmojis({ + application_id: NON_EXISTENT_APPLICATION_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing application as NotFound. Bot tokens + // calling for a different application typically receive Forbidden, + // and malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for an application the bot does not own", async () => { + await runEffect( + listApplicationEmojis({ + application_id: INACCESSIBLE_APPLICATION_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list emojis for its own application; for any + // other application Discord returns Forbidden, but it often returns + // NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listAutoModerationRules.test.ts b/packages/discord/test/listAutoModerationRules.test.ts new file mode 100644 index 000000000..cbfcb3126 --- /dev/null +++ b/packages/discord/test/listAutoModerationRules.test.ts @@ -0,0 +1,93 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { listAutoModerationRules } from "../src/operations/listAutoModerationRules.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/auto-moderation/rules lists all auto-moderation +// rules for a guild. Requires the bot to be a member of the guild and to +// have the MANAGE_GUILD permission. The list is allowed to be empty. + +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real guild. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("listAutoModerationRules", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - lists auto-moderation rules for a guild", + async () => { + const result = await runEffect( + listAutoModerationRules({ guild_id: TEST_GUILD_ID! }), + ); + expect(Array.isArray(result)).toBe(true); + for (const rule of result) { + // The output schema is Schema.Array(Schema.Unknown); validate the + // documented shape defensively. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const r = rule as any; + expect(typeof r.id).toBe("string"); + expect(typeof r.guild_id).toBe("string"); + expect(r.guild_id).toBe(TEST_GUILD_ID!); + expect(typeof r.name).toBe("string"); + expect(typeof r.creator_id).toBe("string"); + expect(typeof r.event_type).toBe("number"); + expect(typeof r.trigger_type).toBe("number"); + expect(Array.isArray(r.actions)).toBe(true); + expect(typeof r.enabled).toBe("boolean"); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent guild_id", async () => { + await runEffect( + listAutoModerationRules({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing guild as NotFound. Bot tokens calling + // for a guild they aren't a member of typically receive Forbidden, + // and malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot is not a member of", async () => { + await runEffect( + listAutoModerationRules({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list auto-moderation rules in guilds it is a + // member of with MANAGE_GUILD; for any other guild Discord returns + // Forbidden, but it often returns NotFound to avoid leaking + // existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listChannelInvites.test.ts b/packages/discord/test/listChannelInvites.test.ts new file mode 100644 index 000000000..0e91c28d3 --- /dev/null +++ b/packages/discord/test/listChannelInvites.test.ts @@ -0,0 +1,110 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { createChannelInvite } from "../src/operations/createChannelInvite.ts"; +import { inviteRevoke } from "../src/operations/inviteRevoke.ts"; +import { listChannelInvites } from "../src/operations/listChannelInvites.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /channels/{channel_id}/invites lists active invites for a channel. +// The happy path creates a fresh invite in an operator-supplied channel +// (DISCORD_TEST_CHANNEL_ID), lists invites, asserts our created code is +// present, then revokes the created invite for cleanup. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-shaped ids unlikely to resolve to any real channel. +const NON_EXISTENT_CHANNEL_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_CHANNEL_ID = "100000000000000001"; + +describe("listChannelInvites", () => { + it( + "happy path - lists invites for a channel including a freshly created one", + async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the listChannelInvites happy path. " + + "Set it to a text/voice channel id where the bot has MANAGE_CHANNELS and CREATE_INSTANT_INVITE.", + ); + } + + const created = await runEffect( + createChannelInvite({ channel_id: TEST_CHANNEL_ID }), + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const code = (created as any).code as string; + expect(typeof code).toBe("string"); + + try { + const result = await runEffect( + listChannelInvites({ channel_id: TEST_CHANNEL_ID }), + ); + expect(Array.isArray(result)).toBe(true); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const codes = (result as any[]).map((inv) => inv.code); + expect(codes).toContain(code); + for (const inv of result) { + // The output schema is Schema.Array(Schema.Unknown); validate the + // documented shape defensively. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const i = inv as any; + expect(typeof i.code).toBe("string"); + if (i.channel && typeof i.channel === "object") { + expect(i.channel.id).toBe(TEST_CHANNEL_ID); + } + } + } finally { + await runEffect(inviteRevoke({ code }).pipe(Effect.ignore)); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent channel_id", async () => { + await runEffect( + listChannelInvites({ channel_id: NON_EXISTENT_CHANNEL_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing channel as NotFound. Bot tokens calling + // for a channel they cannot access typically receive Forbidden, and + // malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a channel the bot cannot access", async () => { + await runEffect( + listChannelInvites({ channel_id: INACCESSIBLE_CHANNEL_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list invites in channels it can MANAGE_CHANNELS; + // for any other channel Discord returns Forbidden, but it often + // returns NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listChannelWebhooks.test.ts b/packages/discord/test/listChannelWebhooks.test.ts new file mode 100644 index 000000000..4cd9286b9 --- /dev/null +++ b/packages/discord/test/listChannelWebhooks.test.ts @@ -0,0 +1,112 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { createWebhook } from "../src/operations/createWebhook.ts"; +import { deleteWebhook } from "../src/operations/deleteWebhook.ts"; +import { listChannelWebhooks } from "../src/operations/listChannelWebhooks.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /channels/{channel_id}/webhooks lists active webhooks for a channel. +// The happy path creates a fresh webhook in an operator-supplied text +// channel (DISCORD_TEST_CHANNEL_ID), lists webhooks, asserts our created +// id is present, then deletes the created webhook on cleanup. The bot +// must have MANAGE_WEBHOOKS in that channel. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-shaped ids unlikely to resolve to any real channel. +const NON_EXISTENT_CHANNEL_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_CHANNEL_ID = "100000000000000001"; + +describe("listChannelWebhooks", () => { + it( + "happy path - lists webhooks for a channel including a freshly created one", + async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the listChannelWebhooks happy path. " + + "Set it to a text channel id where the bot has MANAGE_WEBHOOKS.", + ); + } + const webhookName = `distilled-discord-webhook-${testRunId}`; + const created = await runEffect( + createWebhook({ channel_id: TEST_CHANNEL_ID, name: webhookName }), + ); + const webhookId = created.id; + try { + const result = await runEffect( + listChannelWebhooks({ channel_id: TEST_CHANNEL_ID }), + ); + expect(Array.isArray(result)).toBe(true); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ids = (result as any[]).map((w) => w.id); + expect(ids).toContain(webhookId); + for (const w of result) { + // The output schema is Schema.Array(Schema.Unknown); validate the + // documented webhook shape defensively. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const wh = w as any; + expect(typeof wh.id).toBe("string"); + expect(wh.channel_id).toBe(TEST_CHANNEL_ID); + expect(typeof wh.type).toBe("number"); + if (wh.id === webhookId) { + expect(wh.name).toBe(webhookName); + } + } + } finally { + await runEffect( + deleteWebhook({ webhook_id: webhookId }).pipe(Effect.ignore), + ); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent channel_id", async () => { + await runEffect( + listChannelWebhooks({ channel_id: NON_EXISTENT_CHANNEL_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing channel as NotFound. Bot tokens calling + // for a channel they cannot access typically receive Forbidden, and + // malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a channel the bot cannot access", async () => { + await runEffect( + listChannelWebhooks({ channel_id: INACCESSIBLE_CHANNEL_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list webhooks in channels it has MANAGE_WEBHOOKS; + // for any other channel Discord returns Forbidden, but it often + // returns NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listGuildApplicationCommandPermissions.test.ts b/packages/discord/test/listGuildApplicationCommandPermissions.test.ts new file mode 100644 index 000000000..f41c747a8 --- /dev/null +++ b/packages/discord/test/listGuildApplicationCommandPermissions.test.ts @@ -0,0 +1,106 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { getMyApplication } from "../src/operations/getMyApplication.ts"; +import { listGuildApplicationCommandPermissions } from "../src/operations/listGuildApplicationCommandPermissions.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /applications/{application_id}/guilds/{guild_id}/commands/permissions +// lists per-command permission overrides for the bot's application in a +// guild. The happy path resolves the bot's application id via +// /applications/@me, then lists permissions for an operator-supplied guild +// (DISCORD_TEST_GUILD_ID). The list is allowed to be empty. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real application/guild. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_APPLICATION_ID = "100000000000000001"; + +describe("listGuildApplicationCommandPermissions", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - lists per-command permissions for the bot's application in a guild", + async () => { + const app = await runEffect(getMyApplication({})); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const applicationId = (app as any).id as string; + expect(typeof applicationId).toBe("string"); + + const result = await runEffect( + listGuildApplicationCommandPermissions({ + application_id: applicationId, + guild_id: TEST_GUILD_ID!, + }), + ); + expect(Array.isArray(result)).toBe(true); + for (const entry of result) { + expect(typeof entry.id).toBe("string"); + expect(entry.application_id).toBe(applicationId); + expect(entry.guild_id).toBe(TEST_GUILD_ID!); + expect(Array.isArray(entry.permissions)).toBe(true); + for (const p of entry.permissions) { + expect(typeof p.id).toBe("string"); + expect(typeof p.permission).toBe("boolean"); + } + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent guild_id", async () => { + const app = await runEffect(getMyApplication({})); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const applicationId = (app as any).id as string; + await runEffect( + listGuildApplicationCommandPermissions({ + application_id: applicationId, + guild_id: NON_EXISTENT_GUILD_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing guild as NotFound. Bot tokens calling + // for a guild they aren't a member of typically receive Forbidden, + // and malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for an application the bot does not own", async () => { + await runEffect( + listGuildApplicationCommandPermissions({ + application_id: INACCESSIBLE_APPLICATION_ID, + guild_id: TEST_GUILD_ID ?? NON_EXISTENT_GUILD_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list permissions for its own application; for + // any other application Discord returns Forbidden, but it often + // returns NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listGuildApplicationCommands.test.ts b/packages/discord/test/listGuildApplicationCommands.test.ts new file mode 100644 index 000000000..3977065a2 --- /dev/null +++ b/packages/discord/test/listGuildApplicationCommands.test.ts @@ -0,0 +1,111 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { getMyApplication } from "../src/operations/getMyApplication.ts"; +import { listGuildApplicationCommands } from "../src/operations/listGuildApplicationCommands.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /applications/{application_id}/guilds/{guild_id}/commands lists +// guild-scoped application commands for the bot's application. The happy +// path resolves the bot's application id via /applications/@me, then lists +// commands in an operator-supplied guild (DISCORD_TEST_GUILD_ID). The list +// is allowed to be empty. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real application/guild. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_APPLICATION_ID = "100000000000000001"; + +describe("listGuildApplicationCommands", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - lists guild application commands for the bot's application", + async () => { + const app = await runEffect(getMyApplication({})); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const applicationId = (app as any).id as string; + expect(typeof applicationId).toBe("string"); + + const result = await runEffect( + listGuildApplicationCommands({ + application_id: applicationId, + guild_id: TEST_GUILD_ID!, + with_localizations: true, + }), + ); + expect(Array.isArray(result)).toBe(true); + for (const cmd of result) { + expect(typeof cmd.id).toBe("string"); + expect(cmd.application_id).toBe(applicationId); + expect(typeof cmd.version).toBe("string"); + expect(typeof cmd.name).toBe("string"); + expect(typeof cmd.description).toBe("string"); + expect( + cmd.default_member_permissions === null || + typeof cmd.default_member_permissions === "string", + ).toBe(true); + if (cmd.guild_id !== undefined) { + expect(cmd.guild_id).toBe(TEST_GUILD_ID!); + } + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent guild_id", async () => { + const app = await runEffect(getMyApplication({})); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const applicationId = (app as any).id as string; + await runEffect( + listGuildApplicationCommands({ + application_id: applicationId, + guild_id: NON_EXISTENT_GUILD_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing guild as NotFound. Bot tokens calling + // for a guild they aren't a member of typically receive Forbidden, + // and malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for an application the bot does not own", async () => { + await runEffect( + listGuildApplicationCommands({ + application_id: INACCESSIBLE_APPLICATION_ID, + guild_id: TEST_GUILD_ID ?? NON_EXISTENT_GUILD_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list commands for its own application; for any + // other application Discord returns Forbidden, but it often returns + // NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listGuildAuditLogEntries.test.ts b/packages/discord/test/listGuildAuditLogEntries.test.ts new file mode 100644 index 000000000..0da323d40 --- /dev/null +++ b/packages/discord/test/listGuildAuditLogEntries.test.ts @@ -0,0 +1,97 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { listGuildAuditLogEntries } from "../src/operations/listGuildAuditLogEntries.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/audit-logs returns the audit log for a guild. +// The bot must have VIEW_AUDIT_LOG. The happy path queries the operator- +// supplied test guild (DISCORD_TEST_GUILD_ID) with a small limit. All +// arrays in the response are allowed to be empty. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real guild. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("listGuildAuditLogEntries", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - returns the audit log for a guild", + async () => { + const result = await runEffect( + listGuildAuditLogEntries({ + guild_id: TEST_GUILD_ID!, + limit: 5, + }), + ); + expect(result).toBeDefined(); + expect(Array.isArray(result.audit_log_entries)).toBe(true); + expect(Array.isArray(result.users)).toBe(true); + expect(Array.isArray(result.integrations)).toBe(true); + expect(Array.isArray(result.webhooks)).toBe(true); + expect(Array.isArray(result.guild_scheduled_events)).toBe(true); + expect(Array.isArray(result.threads)).toBe(true); + expect(Array.isArray(result.application_commands)).toBe(true); + expect(Array.isArray(result.auto_moderation_rules)).toBe(true); + expect(result.audit_log_entries.length).toBeLessThanOrEqual(5); + for (const entry of result.audit_log_entries) { + expect(typeof entry.id).toBe("string"); + } + for (const user of result.users) { + expect(typeof user.id).toBe("string"); + expect(typeof user.username).toBe("string"); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent guild_id", async () => { + await runEffect( + listGuildAuditLogEntries({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing guild as NotFound. Bot tokens calling + // for a guild they aren't a member of typically receive Forbidden, + // and malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot cannot access", async () => { + await runEffect( + listGuildAuditLogEntries({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only read the audit log in guilds it's a member of + // with VIEW_AUDIT_LOG; for any other guild Discord returns + // Forbidden, but it often returns NotFound to avoid leaking + // existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listGuildBans.test.ts b/packages/discord/test/listGuildBans.test.ts new file mode 100644 index 000000000..d6987bb82 --- /dev/null +++ b/packages/discord/test/listGuildBans.test.ts @@ -0,0 +1,88 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { listGuildBans } from "../src/operations/listGuildBans.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/bans lists banned users for a guild. Requires +// the bot to have BAN_MEMBERS in the guild. The list is allowed to be +// empty. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real guild. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("listGuildBans", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - lists bans for a guild", + async () => { + const result = await runEffect( + listGuildBans({ guild_id: TEST_GUILD_ID!, limit: 5 }), + ); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeLessThanOrEqual(5); + for (const ban of result) { + expect(typeof ban.user.id).toBe("string"); + expect(typeof ban.user.username).toBe("string"); + expect(typeof ban.user.discriminator).toBe("string"); + expect( + ban.user.avatar === null || typeof ban.user.avatar === "string", + ).toBe(true); + expect(ban.reason === null || typeof ban.reason === "string").toBe( + true, + ); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent guild_id", async () => { + await runEffect( + listGuildBans({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing guild as NotFound. Bot tokens calling + // for a guild they aren't a member of typically receive Forbidden, + // and malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot cannot access", async () => { + await runEffect( + listGuildBans({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list bans in guilds it's a member of with + // BAN_MEMBERS; for any other guild Discord returns Forbidden, but + // it often returns NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listGuildChannels.test.ts b/packages/discord/test/listGuildChannels.test.ts new file mode 100644 index 000000000..898133bf0 --- /dev/null +++ b/packages/discord/test/listGuildChannels.test.ts @@ -0,0 +1,90 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { listGuildChannels } from "../src/operations/listGuildChannels.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/channels lists the non-thread channels in a guild. +// The bot must be a member of the guild. The list is allowed to be empty in +// theory, but a real test guild will always have at least one channel. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real guild. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("listGuildChannels", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - lists channels in a guild", + async () => { + const result = await runEffect( + listGuildChannels({ guild_id: TEST_GUILD_ID! }), + ); + expect(Array.isArray(result)).toBe(true); + for (const ch of result) { + // The output schema is Schema.Array(Schema.Unknown); validate the + // documented channel shape defensively. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const c = ch as any; + expect(typeof c.id).toBe("string"); + expect(typeof c.type).toBe("number"); + if (c.guild_id !== undefined) { + expect(c.guild_id).toBe(TEST_GUILD_ID!); + } + if (c.name !== undefined && c.name !== null) { + expect(typeof c.name).toBe("string"); + } + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent guild_id", async () => { + await runEffect( + listGuildChannels({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing guild as NotFound. Bot tokens calling + // for a guild they aren't a member of typically receive Forbidden, + // and malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot is not a member of", async () => { + await runEffect( + listGuildChannels({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list channels in guilds it's a member of; for + // any other guild Discord returns Forbidden, but it often returns + // NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listGuildEmojis.test.ts b/packages/discord/test/listGuildEmojis.test.ts new file mode 100644 index 000000000..1c6953e85 --- /dev/null +++ b/packages/discord/test/listGuildEmojis.test.ts @@ -0,0 +1,84 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { listGuildEmojis } from "../src/operations/listGuildEmojis.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/emojis lists the custom emojis in a guild. The +// bot must be a member of the guild. The list is allowed to be empty. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real guild. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("listGuildEmojis", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - lists emojis in a guild", + async () => { + const result = await runEffect( + listGuildEmojis({ guild_id: TEST_GUILD_ID! }), + ); + expect(Array.isArray(result)).toBe(true); + for (const emoji of result) { + expect(typeof emoji.id).toBe("string"); + expect(typeof emoji.name).toBe("string"); + expect(Array.isArray(emoji.roles)).toBe(true); + expect(typeof emoji.require_colons).toBe("boolean"); + expect(typeof emoji.managed).toBe("boolean"); + expect(typeof emoji.animated).toBe("boolean"); + expect(typeof emoji.available).toBe("boolean"); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent guild_id", async () => { + await runEffect( + listGuildEmojis({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing guild as NotFound. Bot tokens calling + // for a guild they aren't a member of typically receive Forbidden, + // and malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot is not a member of", async () => { + await runEffect( + listGuildEmojis({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list emojis in guilds it's a member of; for + // any other guild Discord returns Forbidden, but it often returns + // NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listGuildIntegrations.test.ts b/packages/discord/test/listGuildIntegrations.test.ts new file mode 100644 index 000000000..b66bd356f --- /dev/null +++ b/packages/discord/test/listGuildIntegrations.test.ts @@ -0,0 +1,85 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { listGuildIntegrations } from "../src/operations/listGuildIntegrations.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/integrations lists integrations in a guild. +// Requires the bot to be a member of the guild and to have MANAGE_GUILD. +// The list is allowed to be empty. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real guild. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("listGuildIntegrations", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - lists integrations for a guild", + async () => { + const result = await runEffect( + listGuildIntegrations({ guild_id: TEST_GUILD_ID! }), + ); + expect(Array.isArray(result)).toBe(true); + for (const integration of result) { + // The output schema is Schema.Array(Schema.Unknown); validate the + // documented integration shape defensively. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const i = integration as any; + expect(typeof i.id).toBe("string"); + expect(typeof i.name).toBe("string"); + expect(typeof i.type).toBe("string"); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent guild_id", async () => { + await runEffect( + listGuildIntegrations({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing guild as NotFound. Bot tokens calling + // for a guild they aren't a member of typically receive Forbidden, + // and malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot cannot access", async () => { + await runEffect( + listGuildIntegrations({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list integrations in guilds it's a member of + // with MANAGE_GUILD; for any other guild Discord returns Forbidden, + // but it often returns NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listGuildInvites.test.ts b/packages/discord/test/listGuildInvites.test.ts new file mode 100644 index 000000000..c8b81f858 --- /dev/null +++ b/packages/discord/test/listGuildInvites.test.ts @@ -0,0 +1,105 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { createChannelInvite } from "../src/operations/createChannelInvite.ts"; +import { inviteRevoke } from "../src/operations/inviteRevoke.ts"; +import { listGuildInvites } from "../src/operations/listGuildInvites.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/invites lists active invites in a guild. Requires +// MANAGE_GUILD. The happy path creates a fresh channel invite in an +// operator-supplied channel (DISCORD_TEST_CHANNEL_ID) inside the test +// guild (DISCORD_TEST_GUILD_ID), lists guild invites, asserts our created +// code appears, and revokes the created invite for cleanup. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-shaped ids unlikely to resolve to any real guild. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("listGuildInvites", () => { + it.skipIf(!TEST_GUILD_ID || !TEST_CHANNEL_ID)( + "happy path - lists invites in a guild including a freshly created one", + async () => { + const created = await runEffect( + createChannelInvite({ channel_id: TEST_CHANNEL_ID! }), + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const code = (created as any).code as string; + expect(typeof code).toBe("string"); + + try { + const result = await runEffect( + listGuildInvites({ guild_id: TEST_GUILD_ID! }), + ); + expect(Array.isArray(result)).toBe(true); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const codes = (result as any[]).map((inv) => inv.code); + expect(codes).toContain(code); + for (const inv of result) { + // The output schema is Schema.Array(Schema.Unknown); validate the + // documented invite shape defensively. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const i = inv as any; + expect(typeof i.code).toBe("string"); + if (i.guild && typeof i.guild === "object") { + expect(i.guild.id).toBe(TEST_GUILD_ID!); + } + } + } finally { + await runEffect(inviteRevoke({ code }).pipe(Effect.ignore)); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent guild_id", async () => { + await runEffect( + listGuildInvites({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing guild as NotFound. Bot tokens calling + // for a guild they aren't a member of typically receive Forbidden, + // and malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot cannot access", async () => { + await runEffect( + listGuildInvites({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list invites in guilds it's a member of with + // MANAGE_GUILD; for any other guild Discord returns Forbidden, but + // it often returns NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listGuildMembers.test.ts b/packages/discord/test/listGuildMembers.test.ts new file mode 100644 index 000000000..65ecbd2dd --- /dev/null +++ b/packages/discord/test/listGuildMembers.test.ts @@ -0,0 +1,95 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { listGuildMembers } from "../src/operations/listGuildMembers.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/members lists members of a guild. Requires the +// GUILD_MEMBERS privileged gateway intent. The bot must be a member of the +// guild. The list is allowed to be empty in theory, but a real test guild +// will always include at least the bot itself. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real guild. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("listGuildMembers", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - lists members in a guild", + async () => { + const result = await runEffect( + listGuildMembers({ guild_id: TEST_GUILD_ID!, limit: 5 }), + ); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeLessThanOrEqual(5); + for (const member of result) { + expect(typeof member.user.id).toBe("string"); + expect(typeof member.user.username).toBe("string"); + expect(typeof member.joined_at).toBe("string"); + expect(Array.isArray(member.roles)).toBe(true); + expect(typeof member.flags).toBe("number"); + expect(typeof member.pending).toBe("boolean"); + expect(typeof member.mute).toBe("boolean"); + expect(typeof member.deaf).toBe("boolean"); + expect(member.nick === null || typeof member.nick === "string").toBe( + true, + ); + expect( + member.avatar === null || typeof member.avatar === "string", + ).toBe(true); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent guild_id", async () => { + await runEffect( + listGuildMembers({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing guild as NotFound. Bot tokens calling + // for a guild they aren't a member of typically receive Forbidden, + // and malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot is not a member of", async () => { + await runEffect( + listGuildMembers({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list members in guilds it's a member of, and + // requires the GUILD_MEMBERS privileged intent; for any other + // guild Discord returns Forbidden, but it often returns NotFound + // to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listGuildRoles.test.ts b/packages/discord/test/listGuildRoles.test.ts new file mode 100644 index 000000000..ff85bb9a3 --- /dev/null +++ b/packages/discord/test/listGuildRoles.test.ts @@ -0,0 +1,96 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { listGuildRoles } from "../src/operations/listGuildRoles.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/roles lists the roles in a guild. The bot must +// be a member of the guild. Every guild has at least the @everyone role, +// so a real test guild will always return at least one entry. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real guild. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("listGuildRoles", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - lists roles in a guild", + async () => { + const result = await runEffect( + listGuildRoles({ guild_id: TEST_GUILD_ID! }), + ); + expect(Array.isArray(result)).toBe(true); + // Every guild has @everyone. + expect(result.length).toBeGreaterThanOrEqual(1); + for (const role of result) { + expect(typeof role.id).toBe("string"); + expect(typeof role.name).toBe("string"); + expect( + role.description === null || typeof role.description === "string", + ).toBe(true); + expect(typeof role.permissions).toBe("string"); + expect(typeof role.position).toBe("number"); + expect(typeof role.color).toBe("number"); + expect(typeof role.colors.primary_color).toBe("number"); + expect(typeof role.hoist).toBe("boolean"); + expect(typeof role.managed).toBe("boolean"); + expect(typeof role.mentionable).toBe("boolean"); + expect(typeof role.flags).toBe("number"); + } + // The @everyone role's id equals the guild id. + const everyone = result.find((r) => r.id === TEST_GUILD_ID!); + expect(everyone).toBeDefined(); + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent guild_id", async () => { + await runEffect( + listGuildRoles({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing guild as NotFound. Bot tokens calling + // for a guild they aren't a member of typically receive Forbidden, + // and malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot is not a member of", async () => { + await runEffect( + listGuildRoles({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list roles in guilds it's a member of; for any + // other guild Discord returns Forbidden, but it often returns + // NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listGuildScheduledEventUsers.test.ts b/packages/discord/test/listGuildScheduledEventUsers.test.ts new file mode 100644 index 000000000..9d23cd5d6 --- /dev/null +++ b/packages/discord/test/listGuildScheduledEventUsers.test.ts @@ -0,0 +1,106 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { listGuildScheduledEventUsers } from "../src/operations/listGuildScheduledEventUsers.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/scheduled-events/{guild_scheduled_event_id}/users +// lists users subscribed to a guild scheduled event. The bot must be a +// member of the guild. The list is allowed to be empty. The happy path +// requires the operator to supply DISCORD_TEST_GUILD_ID plus +// DISCORD_TEST_SCHEDULED_EVENT_ID for an event in that guild. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_SCHEDULED_EVENT_ID = process.env.DISCORD_TEST_SCHEDULED_EVENT_ID; + +// Snowflake-shaped ids unlikely to resolve to any real guild/event. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const NON_EXISTENT_EVENT_ID = "100000000000000001"; +const INACCESSIBLE_GUILD_ID = "100000000000000002"; + +describe("listGuildScheduledEventUsers", () => { + it.skipIf(!TEST_GUILD_ID || !TEST_SCHEDULED_EVENT_ID)( + "happy path - lists users subscribed to a guild scheduled event", + async () => { + const result = await runEffect( + listGuildScheduledEventUsers({ + guild_id: TEST_GUILD_ID!, + guild_scheduled_event_id: TEST_SCHEDULED_EVENT_ID!, + with_member: true, + limit: 5, + }), + ); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeLessThanOrEqual(5); + for (const entry of result) { + expect(entry.guild_scheduled_event_id).toBe(TEST_SCHEDULED_EVENT_ID!); + expect(typeof entry.user_id).toBe("string"); + if (entry.user) { + expect(entry.user.id).toBe(entry.user_id); + expect(typeof entry.user.username).toBe("string"); + } + if (entry.member) { + expect(typeof entry.member.joined_at).toBe("string"); + expect(Array.isArray(entry.member.roles)).toBe(true); + expect(entry.member.user.id).toBe(entry.user_id); + } + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent scheduled event", async () => { + await runEffect( + listGuildScheduledEventUsers({ + guild_id: TEST_GUILD_ID ?? NON_EXISTENT_GUILD_ID, + guild_scheduled_event_id: NON_EXISTENT_EVENT_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing event (or guild) as NotFound. Bot + // tokens calling for a guild they aren't a member of typically + // receive Forbidden, and malformed snowflakes may surface as + // BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot is not a member of", async () => { + await runEffect( + listGuildScheduledEventUsers({ + guild_id: INACCESSIBLE_GUILD_ID, + guild_scheduled_event_id: NON_EXISTENT_EVENT_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list event users in guilds it's a member of; + // for any other guild Discord returns Forbidden, but it often + // returns NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listGuildScheduledEvents.test.ts b/packages/discord/test/listGuildScheduledEvents.test.ts new file mode 100644 index 000000000..95220aa84 --- /dev/null +++ b/packages/discord/test/listGuildScheduledEvents.test.ts @@ -0,0 +1,89 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { listGuildScheduledEvents } from "../src/operations/listGuildScheduledEvents.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/scheduled-events lists scheduled events for a +// guild. The bot must be a member of the guild. The list is allowed to +// be empty. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real guild. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("listGuildScheduledEvents", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - lists scheduled events for a guild", + async () => { + const result = await runEffect( + listGuildScheduledEvents({ + guild_id: TEST_GUILD_ID!, + with_user_count: true, + }), + ); + expect(Array.isArray(result)).toBe(true); + for (const event of result) { + // The output schema is Schema.Array(Schema.Unknown); validate the + // documented scheduled event shape defensively. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const e = event as any; + expect(typeof e.id).toBe("string"); + expect(e.guild_id).toBe(TEST_GUILD_ID!); + expect(typeof e.name).toBe("string"); + expect(typeof e.scheduled_start_time).toBe("string"); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent guild_id", async () => { + await runEffect( + listGuildScheduledEvents({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing guild as NotFound. Bot tokens calling + // for a guild they aren't a member of typically receive Forbidden, + // and malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot is not a member of", async () => { + await runEffect( + listGuildScheduledEvents({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list scheduled events in guilds it's a member + // of; for any other guild Discord returns Forbidden, but it often + // returns NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listGuildSoundboardSounds.test.ts b/packages/discord/test/listGuildSoundboardSounds.test.ts new file mode 100644 index 000000000..5ce60545b --- /dev/null +++ b/packages/discord/test/listGuildSoundboardSounds.test.ts @@ -0,0 +1,89 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { listGuildSoundboardSounds } from "../src/operations/listGuildSoundboardSounds.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/soundboard-sounds lists soundboard sounds for a +// guild. The bot must be a member of the guild. The list is allowed to +// be empty. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real guild. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("listGuildSoundboardSounds", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - lists soundboard sounds for a guild", + async () => { + const result = await runEffect( + listGuildSoundboardSounds({ guild_id: TEST_GUILD_ID! }), + ); + expect(result).toBeDefined(); + expect(Array.isArray(result.items)).toBe(true); + for (const sound of result.items) { + expect(typeof sound.name).toBe("string"); + expect(typeof sound.sound_id).toBe("string"); + expect(typeof sound.volume).toBe("number"); + expect(typeof sound.available).toBe("boolean"); + expect( + sound.emoji_name === null || typeof sound.emoji_name === "string", + ).toBe(true); + if (sound.guild_id !== undefined) { + expect(sound.guild_id).toBe(TEST_GUILD_ID!); + } + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent guild_id", async () => { + await runEffect( + listGuildSoundboardSounds({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing guild as NotFound. Bot tokens calling + // for a guild they aren't a member of typically receive Forbidden, + // and malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot is not a member of", async () => { + await runEffect( + listGuildSoundboardSounds({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list soundboard sounds in guilds it's a member + // of; for any other guild Discord returns Forbidden, but it often + // returns NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listGuildStickers.test.ts b/packages/discord/test/listGuildStickers.test.ts new file mode 100644 index 000000000..91d26abf4 --- /dev/null +++ b/packages/discord/test/listGuildStickers.test.ts @@ -0,0 +1,86 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { listGuildStickers } from "../src/operations/listGuildStickers.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/stickers lists custom stickers in a guild. The +// bot must be a member of the guild. The list is allowed to be empty. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real guild. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("listGuildStickers", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - lists stickers in a guild", + async () => { + const result = await runEffect( + listGuildStickers({ guild_id: TEST_GUILD_ID! }), + ); + expect(Array.isArray(result)).toBe(true); + for (const sticker of result) { + expect(typeof sticker.id).toBe("string"); + expect(typeof sticker.name).toBe("string"); + expect(typeof sticker.tags).toBe("string"); + expect( + sticker.description === null || + typeof sticker.description === "string", + ).toBe(true); + expect(typeof sticker.available).toBe("boolean"); + expect(sticker.guild_id).toBe(TEST_GUILD_ID!); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent guild_id", async () => { + await runEffect( + listGuildStickers({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing guild as NotFound. Bot tokens calling + // for a guild they aren't a member of typically receive Forbidden, + // and malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot is not a member of", async () => { + await runEffect( + listGuildStickers({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list stickers in guilds it's a member of; for + // any other guild Discord returns Forbidden, but it often returns + // NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listGuildTemplates.test.ts b/packages/discord/test/listGuildTemplates.test.ts new file mode 100644 index 000000000..8066e3bae --- /dev/null +++ b/packages/discord/test/listGuildTemplates.test.ts @@ -0,0 +1,94 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { listGuildTemplates } from "../src/operations/listGuildTemplates.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/templates lists guild templates. Requires the bot +// to have MANAGE_GUILD in the guild. The list is allowed to be empty. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real guild. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("listGuildTemplates", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - lists templates for a guild", + async () => { + const result = await runEffect( + listGuildTemplates({ guild_id: TEST_GUILD_ID! }), + ); + expect(Array.isArray(result)).toBe(true); + for (const tpl of result) { + expect(typeof tpl.code).toBe("string"); + expect(typeof tpl.name).toBe("string"); + expect( + tpl.description === null || typeof tpl.description === "string", + ).toBe(true); + expect(typeof tpl.usage_count).toBe("number"); + expect(typeof tpl.creator_id).toBe("string"); + expect(typeof tpl.created_at).toBe("string"); + expect(typeof tpl.updated_at).toBe("string"); + expect(tpl.source_guild_id).toBe(TEST_GUILD_ID!); + expect(tpl.serialized_source_guild).toBeDefined(); + expect(typeof tpl.serialized_source_guild.name).toBe("string"); + expect(Array.isArray(tpl.serialized_source_guild.roles)).toBe(true); + expect(Array.isArray(tpl.serialized_source_guild.channels)).toBe(true); + expect(tpl.is_dirty === null || typeof tpl.is_dirty === "boolean").toBe( + true, + ); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent guild_id", async () => { + await runEffect( + listGuildTemplates({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing guild as NotFound. Bot tokens calling + // for a guild they aren't a member of typically receive Forbidden, + // and malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot cannot access", async () => { + await runEffect( + listGuildTemplates({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list templates in guilds it's a member of with + // MANAGE_GUILD; for any other guild Discord returns Forbidden, but + // it often returns NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listGuildVoiceRegions.test.ts b/packages/discord/test/listGuildVoiceRegions.test.ts new file mode 100644 index 000000000..12101117b --- /dev/null +++ b/packages/discord/test/listGuildVoiceRegions.test.ts @@ -0,0 +1,84 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { listGuildVoiceRegions } from "../src/operations/listGuildVoiceRegions.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/regions lists voice regions available to a guild +// (including any VIP-only regions). The bot must be a member of the guild. +// Discord always advertises at least one region. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to any real guild. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("listGuildVoiceRegions", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - lists voice regions available to a guild", + async () => { + const result = await runEffect( + listGuildVoiceRegions({ guild_id: TEST_GUILD_ID! }), + ); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeGreaterThanOrEqual(1); + for (const region of result) { + expect(typeof region.id).toBe("string"); + expect(typeof region.name).toBe("string"); + expect(typeof region.custom).toBe("boolean"); + expect(typeof region.deprecated).toBe("boolean"); + expect(typeof region.optimal).toBe("boolean"); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent guild_id", async () => { + await runEffect( + listGuildVoiceRegions({ guild_id: NON_EXISTENT_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing guild as NotFound. Bot tokens calling + // for a guild they aren't a member of typically receive Forbidden, + // and malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a guild the bot is not a member of", async () => { + await runEffect( + listGuildVoiceRegions({ guild_id: INACCESSIBLE_GUILD_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list voice regions in guilds it's a member of; + // for any other guild Discord returns Forbidden, but it often + // returns NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listMessageReactionsByEmoji.test.ts b/packages/discord/test/listMessageReactionsByEmoji.test.ts new file mode 100644 index 000000000..0c7261dcb --- /dev/null +++ b/packages/discord/test/listMessageReactionsByEmoji.test.ts @@ -0,0 +1,144 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { addMyMessageReaction } from "../src/operations/addMyMessageReaction.ts"; +import { createMessage } from "../src/operations/createMessage.ts"; +import { deleteMessage } from "../src/operations/deleteMessage.ts"; +import { getMyUser } from "../src/operations/getMyUser.ts"; +import { listMessageReactionsByEmoji } from "../src/operations/listMessageReactionsByEmoji.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /channels/{channel_id}/messages/{message_id}/reactions/{emoji_name} +// lists users who reacted to a message with a given emoji. The happy path +// posts a fresh message in an operator-supplied text channel +// (DISCORD_TEST_CHANNEL_ID), adds the bot's own reaction with a unicode +// emoji, lists reactors, asserts the bot's id is included, then deletes +// the message for cleanup. The bot must have SEND_MESSAGES, +// ADD_REACTIONS, and READ_MESSAGE_HISTORY in that channel. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// A simple unicode emoji (thumbs up). +const EMOJI = "\u{1F44D}"; + +// Snowflake-shaped ids unlikely to resolve to any real channel/message. +const NON_EXISTENT_CHANNEL_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const NON_EXISTENT_MESSAGE_ID = "100000000000000001"; +const INACCESSIBLE_CHANNEL_ID = "100000000000000002"; + +describe("listMessageReactionsByEmoji", () => { + it.skipIf(!TEST_CHANNEL_ID)( + "happy path - lists users who reacted with a given emoji", + async () => { + const me = await runEffect(getMyUser({})); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const myId = (me as any).id as string; + expect(typeof myId).toBe("string"); + + const created = await runEffect( + createMessage({ + channel_id: TEST_CHANNEL_ID!, + content: `distilled-discord reaction list test ${testRunId}`, + }), + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const messageId = (created as any).id as string; + expect(typeof messageId).toBe("string"); + + try { + await runEffect( + addMyMessageReaction({ + channel_id: TEST_CHANNEL_ID!, + message_id: messageId, + emoji_name: EMOJI, + }), + ); + + const result = await runEffect( + listMessageReactionsByEmoji({ + channel_id: TEST_CHANNEL_ID!, + message_id: messageId, + emoji_name: EMOJI, + limit: 25, + }), + ); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeGreaterThanOrEqual(1); + const ids = result.map((u) => u.id); + expect(ids).toContain(myId); + for (const user of result) { + expect(typeof user.id).toBe("string"); + expect(typeof user.username).toBe("string"); + expect(typeof user.discriminator).toBe("string"); + expect( + user.avatar === null || typeof user.avatar === "string", + ).toBe(true); + } + } finally { + await runEffect( + deleteMessage({ + channel_id: TEST_CHANNEL_ID!, + message_id: messageId, + }).pipe(Effect.ignore), + ); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent message", async () => { + await runEffect( + listMessageReactionsByEmoji({ + channel_id: TEST_CHANNEL_ID ?? NON_EXISTENT_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + emoji_name: EMOJI, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing message (or channel) as NotFound. Bot + // tokens calling for a channel they cannot access typically receive + // Forbidden, and malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a channel the bot cannot access", async () => { + await runEffect( + listMessageReactionsByEmoji({ + channel_id: INACCESSIBLE_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + emoji_name: EMOJI, + }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list reactions in channels it can read; for any + // other channel Discord returns Forbidden, but it often returns + // NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listMessages.test.ts b/packages/discord/test/listMessages.test.ts new file mode 100644 index 000000000..2efd51695 --- /dev/null +++ b/packages/discord/test/listMessages.test.ts @@ -0,0 +1,123 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { createMessage } from "../src/operations/createMessage.ts"; +import { deleteMessage } from "../src/operations/deleteMessage.ts"; +import { listMessages } from "../src/operations/listMessages.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /channels/{channel_id}/messages lists messages in a channel. The +// happy path posts a fresh message in an operator-supplied text channel +// (DISCORD_TEST_CHANNEL_ID), lists messages, asserts the new message id +// is present, then deletes the message on cleanup. The bot must have +// SEND_MESSAGES and READ_MESSAGE_HISTORY in that channel. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-shaped ids unlikely to resolve to any real channel. +const NON_EXISTENT_CHANNEL_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_CHANNEL_ID = "100000000000000001"; + +describe("listMessages", () => { + it.skipIf(!TEST_CHANNEL_ID)( + "happy path - lists messages including a freshly posted one", + async () => { + const content = `distilled-discord listMessages test ${testRunId}`; + const created = await runEffect( + createMessage({ channel_id: TEST_CHANNEL_ID!, content }), + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const messageId = (created as any).id as string; + expect(typeof messageId).toBe("string"); + + try { + const result = await runEffect( + listMessages({ channel_id: TEST_CHANNEL_ID!, limit: 25 }), + ); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeGreaterThanOrEqual(1); + const ids = result.map((m) => m.id); + expect(ids).toContain(messageId); + for (const m of result) { + expect(typeof m.id).toBe("string"); + expect(m.channel_id).toBe(TEST_CHANNEL_ID!); + expect(typeof m.content).toBe("string"); + expect(typeof m.timestamp).toBe("string"); + expect(typeof m.author.id).toBe("string"); + expect(typeof m.author.username).toBe("string"); + expect(typeof m.pinned).toBe("boolean"); + expect(typeof m.tts).toBe("boolean"); + expect(typeof m.mention_everyone).toBe("boolean"); + expect(Array.isArray(m.mentions)).toBe(true); + expect(Array.isArray(m.mention_roles)).toBe(true); + expect(Array.isArray(m.attachments)).toBe(true); + expect(Array.isArray(m.embeds)).toBe(true); + expect( + m.edited_timestamp === null || + typeof m.edited_timestamp === "string", + ).toBe(true); + if (m.id === messageId) { + expect(m.content).toBe(content); + } + } + } finally { + await runEffect( + deleteMessage({ + channel_id: TEST_CHANNEL_ID!, + message_id: messageId, + }).pipe(Effect.ignore), + ); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent channel_id", async () => { + await runEffect( + listMessages({ channel_id: NON_EXISTENT_CHANNEL_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord surfaces a missing channel as NotFound. Bot tokens + // calling for a channel they cannot access typically receive + // Forbidden, and malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a channel the bot cannot access", async () => { + await runEffect( + listMessages({ channel_id: INACCESSIBLE_CHANNEL_ID }).pipe( + Effect.flip, + Effect.map((e) => { + // The bot can only list messages in channels it can read with + // READ_MESSAGE_HISTORY; for any other channel Discord returns + // Forbidden, but it often returns NotFound to avoid leaking + // existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listMyConnections.test.ts b/packages/discord/test/listMyConnections.test.ts new file mode 100644 index 000000000..8cf583e21 --- /dev/null +++ b/packages/discord/test/listMyConnections.test.ts @@ -0,0 +1,123 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as Redacted from "effect/Redacted"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { + Credentials, + CredentialsFromEnv, + DEFAULT_API_BASE_URL, +} from "../src/credentials.ts"; +import { listMyConnections } from "../src/operations/listMyConnections.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /users/@me/connections lists OAuth2 connections for the calling user. +// This endpoint is user-only and requires a Bearer token with the +// `connections` scope; bot tokens are rejected. The happy path is gated on +// DISCORD_BEARER_TOKEN. +const TEST_BEARER = process.env.DISCORD_BEARER_TOKEN; + +// A deliberately bogus bearer token used for the error tests so they don't +// depend on operator credentials. +const makeInvalidBearerLayer = (token: string): Layer.Layer => + Layer.succeed(Credentials, { + token: Redacted.make(token), + authScheme: "Bearer" as const, + apiBaseUrl: DEFAULT_API_BASE_URL, + }); + +const runWithInvalidBearer = ( + token: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + effect: Effect.Effect, +): Promise => { + const layer = Layer.merge( + makeInvalidBearerLayer(token), + FetchHttpClient.layer, + ); + return Effect.runPromise( + effect.pipe(Effect.provide(layer)) as Effect.Effect, + ); +}; + +describe("listMyConnections", () => { + it.skipIf(!TEST_BEARER)( + "happy path - lists OAuth2 connections for the calling user", + async () => { + const result = await runEffect(listMyConnections({})); + expect(Array.isArray(result)).toBe(true); + for (const conn of result) { + expect(typeof conn.id).toBe("string"); + expect(conn.name === null || typeof conn.name === "string").toBe(true); + expect(typeof conn.friend_sync).toBe("boolean"); + expect(typeof conn.show_activity).toBe("boolean"); + expect(typeof conn.two_way_link).toBe("boolean"); + expect(typeof conn.verified).toBe("boolean"); + if (conn.revoked !== undefined) { + expect(typeof conn.revoked).toBe("boolean"); + } + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound or Forbidden for an invalid bearer token", async () => { + await runWithInvalidBearer( + `invalid-bearer-${testRunId}`, + listMyConnections({}).pipe( + Effect.flip, + Effect.map((e) => { + // /users/@me/connections is OAuth2-only. An invalid bearer token + // typically surfaces as Unauthorized; a token without the + // `connections` scope surfaces as Forbidden; some routes return + // NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect([ + "NotFound", + "Forbidden", + "Unauthorized", + "BadRequest", + ]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a bearer token missing the connections scope", async () => { + // A second deliberately bogus token, framing this scenario as a token + // that lacks the required scope. Discord may surface this as Forbidden + // or NotFound depending on routing. + await runWithInvalidBearer( + `no-scope-${testRunId}`, + listMyConnections({}).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect([ + "Forbidden", + "NotFound", + "Unauthorized", + "BadRequest", + ]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listMyGuilds.test.ts b/packages/discord/test/listMyGuilds.test.ts new file mode 100644 index 000000000..694735138 --- /dev/null +++ b/packages/discord/test/listMyGuilds.test.ts @@ -0,0 +1,138 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as Redacted from "effect/Redacted"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { + Credentials, + CredentialsFromEnv, + DEFAULT_API_BASE_URL, +} from "../src/credentials.ts"; +import { listMyGuilds } from "../src/operations/listMyGuilds.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /users/@me/guilds lists guilds the calling user/bot is a member of. +// Bot tokens return the guilds the bot has been invited to. Bearer tokens +// require the `guilds` scope. + +// Override the credentials layer with a deliberately bogus bearer token to +// drive the error tests so they don't depend on operator credentials. +const makeInvalidBearerLayer = (token: string): Layer.Layer => + Layer.succeed(Credentials, { + token: Redacted.make(token), + authScheme: "Bearer" as const, + apiBaseUrl: DEFAULT_API_BASE_URL, + }); + +const runWithInvalidBearer = ( + token: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + effect: Effect.Effect, +): Promise => { + const layer = Layer.merge( + makeInvalidBearerLayer(token), + FetchHttpClient.layer, + ); + return Effect.runPromise( + effect.pipe(Effect.provide(layer)) as Effect.Effect, + ); +}; + +describe("listMyGuilds", () => { + it( + "happy path - lists guilds for the calling token", + async () => { + const result = await runEffect( + listMyGuilds({ limit: 5, with_counts: true }), + ); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeLessThanOrEqual(5); + for (const guild of result) { + expect(typeof guild.id).toBe("string"); + expect(typeof guild.name).toBe("string"); + expect( + guild.icon === null || typeof guild.icon === "string", + ).toBe(true); + expect( + guild.banner === null || typeof guild.banner === "string", + ).toBe(true); + expect(typeof guild.owner).toBe("boolean"); + expect(typeof guild.permissions).toBe("string"); + expect(Array.isArray(guild.features)).toBe(true); + if (guild.approximate_member_count !== undefined) { + expect( + guild.approximate_member_count === null || + typeof guild.approximate_member_count === "number", + ).toBe(true); + } + if (guild.approximate_presence_count !== undefined) { + expect( + guild.approximate_presence_count === null || + typeof guild.approximate_presence_count === "number", + ).toBe(true); + } + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound or Forbidden for an invalid bearer token", async () => { + await runWithInvalidBearer( + `invalid-bearer-${testRunId}`, + listMyGuilds({}).pipe( + Effect.flip, + Effect.map((e) => { + // /users/@me/guilds rejects invalid bearer tokens. An invalid + // token typically surfaces as Unauthorized; a token without the + // `guilds` scope surfaces as Forbidden; some routes return + // NotFound to avoid leaking existence. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect([ + "NotFound", + "Forbidden", + "Unauthorized", + "BadRequest", + ]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a bearer token missing the guilds scope", async () => { + // A second deliberately bogus token, framing this scenario as a token + // that lacks the required scope. Discord may surface this as Forbidden + // or NotFound depending on routing. + await runWithInvalidBearer( + `no-scope-${testRunId}`, + listMyGuilds({}).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect([ + "Forbidden", + "NotFound", + "Unauthorized", + "BadRequest", + ]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listMyPrivateArchivedThreads.test.ts b/packages/discord/test/listMyPrivateArchivedThreads.test.ts new file mode 100644 index 000000000..269090cb1 --- /dev/null +++ b/packages/discord/test/listMyPrivateArchivedThreads.test.ts @@ -0,0 +1,112 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { listMyPrivateArchivedThreads } from "../src/operations/listMyPrivateArchivedThreads.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /channels/{channel_id}/users/@me/threads/archived/private lists private +// archived threads in a channel that the calling user has joined. Requires +// READ_MESSAGE_HISTORY on the channel. The happy path is gated on +// DISCORD_TEST_CHANNEL_ID since it needs a real text channel id. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +describe("listMyPrivateArchivedThreads", () => { + it.skipIf(!TEST_CHANNEL_ID)( + "happy path - lists private archived threads the caller has joined", + async () => { + const result = await runEffect( + listMyPrivateArchivedThreads({ + channel_id: TEST_CHANNEL_ID!, + limit: 5, + }), + ); + expect(Array.isArray(result.threads)).toBe(true); + expect(Array.isArray(result.members)).toBe(true); + expect(typeof result.has_more).toBe("boolean"); + for (const thread of result.threads) { + expect(typeof thread.id).toBe("string"); + expect(typeof thread.name).toBe("string"); + if (thread.guild_id !== undefined && thread.guild_id !== null) { + expect(typeof thread.guild_id).toBe("string"); + } + if (thread.owner_id !== undefined && thread.owner_id !== null) { + expect(typeof thread.owner_id).toBe("string"); + } + if (thread.thread_metadata !== undefined) { + expect(typeof thread.thread_metadata.archived).toBe("boolean"); + expect(typeof thread.thread_metadata.locked).toBe("boolean"); + expect(typeof thread.thread_metadata.archive_timestamp).toBe( + "string", + ); + } + if (thread.message_count !== undefined && thread.message_count !== null) { + expect(typeof thread.message_count).toBe("number"); + } + if (thread.member_count !== undefined && thread.member_count !== null) { + expect(typeof thread.member_count).toBe("number"); + } + } + for (const member of result.members) { + if (member.id !== undefined && member.id !== null) { + expect(typeof member.id).toBe("string"); + } + if (member.user_id !== undefined && member.user_id !== null) { + expect(typeof member.user_id).toBe("string"); + } + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent channel id", async () => { + const fakeChannelId = `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + listMyPrivateArchivedThreads({ channel_id: fakeChannelId }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (channel does not exist) or + // Forbidden (bot cannot see the channel) or BadRequest depending + // on routing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for an inaccessible channel", async () => { + // A snowflake the bot is unlikely to have access to. Discord may surface + // this as Forbidden, NotFound (to avoid leaking existence), or BadRequest. + const inaccessibleChannelId = "100000000000000001"; + await runEffect( + listMyPrivateArchivedThreads({ + channel_id: inaccessibleChannelId, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listPins.test.ts b/packages/discord/test/listPins.test.ts new file mode 100644 index 000000000..b782e7cee --- /dev/null +++ b/packages/discord/test/listPins.test.ts @@ -0,0 +1,83 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { listPins } from "../src/operations/listPins.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /channels/{channel_id}/messages/pins lists pinned messages in a channel. +// Requires READ_MESSAGES on the channel. Happy path is gated on a real +// DISCORD_TEST_CHANNEL_ID since it requires a real channel the bot can read. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +describe("listPins", () => { + it.skipIf(!TEST_CHANNEL_ID)( + "happy path - lists pinned messages in a channel", + async () => { + const result = await runEffect( + listPins({ channel_id: TEST_CHANNEL_ID!, limit: 5 }), + ); + expect(Array.isArray(result.items)).toBe(true); + for (const pin of result.items) { + expect(typeof pin.pinned_at).toBe("string"); + expect(typeof pin.message.content).toBe("string"); + expect(Array.isArray(pin.message.mentions)).toBe(true); + expect(Array.isArray(pin.message.mention_roles)).toBe(true); + expect(Array.isArray(pin.message.attachments)).toBe(true); + for (const mention of pin.message.mentions) { + expect(typeof mention.id).toBe("string"); + expect(typeof mention.username).toBe("string"); + } + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent channel id", async () => { + const fakeChannelId = `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + listPins({ channel_id: fakeChannelId }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (channel does not exist), Forbidden + // (bot cannot see the channel), or BadRequest depending on routing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for an inaccessible channel", async () => { + // A snowflake the bot is unlikely to have access to. Discord may surface + // this as Forbidden, NotFound (to avoid leaking existence), or BadRequest. + const inaccessibleChannelId = "100000000000000001"; + await runEffect( + listPins({ channel_id: inaccessibleChannelId }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listPrivateArchivedThreads.test.ts b/packages/discord/test/listPrivateArchivedThreads.test.ts new file mode 100644 index 000000000..fc3f4ce15 --- /dev/null +++ b/packages/discord/test/listPrivateArchivedThreads.test.ts @@ -0,0 +1,107 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { listPrivateArchivedThreads } from "../src/operations/listPrivateArchivedThreads.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /channels/{channel_id}/threads/archived/private lists all private +// archived threads in a channel. Requires READ_MESSAGE_HISTORY and +// MANAGE_THREADS. Happy path is gated on DISCORD_TEST_CHANNEL_ID. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +describe("listPrivateArchivedThreads", () => { + it.skipIf(!TEST_CHANNEL_ID)( + "happy path - lists private archived threads in a channel", + async () => { + const result = await runEffect( + listPrivateArchivedThreads({ + channel_id: TEST_CHANNEL_ID!, + limit: 5, + }), + ); + expect(Array.isArray(result.threads)).toBe(true); + expect(Array.isArray(result.members)).toBe(true); + expect(typeof result.has_more).toBe("boolean"); + for (const thread of result.threads) { + expect(typeof thread.id).toBe("string"); + expect(typeof thread.flags).toBe("number"); + if (thread.name !== undefined && thread.name !== null) { + expect(typeof thread.name).toBe("string"); + } + if (thread.guild_id !== undefined && thread.guild_id !== null) { + expect(typeof thread.guild_id).toBe("string"); + } + if (thread.owner_id !== undefined && thread.owner_id !== null) { + expect(typeof thread.owner_id).toBe("string"); + } + if (thread.thread_metadata !== undefined) { + expect(typeof thread.thread_metadata.archived).toBe("boolean"); + expect(typeof thread.thread_metadata.locked).toBe("boolean"); + expect(typeof thread.thread_metadata.archive_timestamp).toBe( + "string", + ); + } + } + for (const member of result.members) { + if (member.id !== undefined && member.id !== null) { + expect(typeof member.id).toBe("string"); + } + if (member.user_id !== undefined && member.user_id !== null) { + expect(typeof member.user_id).toBe("string"); + } + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent channel id", async () => { + const fakeChannelId = `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + listPrivateArchivedThreads({ channel_id: fakeChannelId }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (channel does not exist), Forbidden + // (bot cannot see the channel), or BadRequest depending on routing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for an inaccessible channel", async () => { + // A snowflake the bot is unlikely to have access to. Discord may surface + // this as Forbidden, NotFound (to avoid leaking existence), or BadRequest. + const inaccessibleChannelId = "100000000000000001"; + await runEffect( + listPrivateArchivedThreads({ + channel_id: inaccessibleChannelId, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listPublicArchivedThreads.test.ts b/packages/discord/test/listPublicArchivedThreads.test.ts new file mode 100644 index 000000000..adcc2fe27 --- /dev/null +++ b/packages/discord/test/listPublicArchivedThreads.test.ts @@ -0,0 +1,107 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { listPublicArchivedThreads } from "../src/operations/listPublicArchivedThreads.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /channels/{channel_id}/threads/archived/public lists all public archived +// threads in a channel. Requires READ_MESSAGE_HISTORY. Happy path is gated on +// DISCORD_TEST_CHANNEL_ID since it requires a real channel id. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +describe("listPublicArchivedThreads", () => { + it.skipIf(!TEST_CHANNEL_ID)( + "happy path - lists public archived threads in a channel", + async () => { + const result = await runEffect( + listPublicArchivedThreads({ + channel_id: TEST_CHANNEL_ID!, + limit: 5, + }), + ); + expect(Array.isArray(result.threads)).toBe(true); + expect(Array.isArray(result.members)).toBe(true); + expect(typeof result.has_more).toBe("boolean"); + for (const thread of result.threads) { + expect(typeof thread.id).toBe("string"); + expect(typeof thread.flags).toBe("number"); + if (thread.name !== undefined && thread.name !== null) { + expect(typeof thread.name).toBe("string"); + } + if (thread.guild_id !== undefined && thread.guild_id !== null) { + expect(typeof thread.guild_id).toBe("string"); + } + if (thread.owner_id !== undefined && thread.owner_id !== null) { + expect(typeof thread.owner_id).toBe("string"); + } + if (thread.thread_metadata !== undefined) { + expect(typeof thread.thread_metadata.archived).toBe("boolean"); + expect(typeof thread.thread_metadata.locked).toBe("boolean"); + expect(typeof thread.thread_metadata.archive_timestamp).toBe( + "string", + ); + } + } + for (const member of result.members) { + if (member.id !== undefined && member.id !== null) { + expect(typeof member.id).toBe("string"); + } + if (member.user_id !== undefined && member.user_id !== null) { + expect(typeof member.user_id).toBe("string"); + } + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent channel id", async () => { + const fakeChannelId = `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + listPublicArchivedThreads({ channel_id: fakeChannelId }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (channel does not exist), Forbidden + // (bot cannot see the channel), or BadRequest depending on routing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for an inaccessible channel", async () => { + // A snowflake the bot is unlikely to have access to. Discord may surface + // this as Forbidden, NotFound (to avoid leaking existence), or BadRequest. + const inaccessibleChannelId = "100000000000000001"; + await runEffect( + listPublicArchivedThreads({ + channel_id: inaccessibleChannelId, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listStickerPacks.test.ts b/packages/discord/test/listStickerPacks.test.ts new file mode 100644 index 000000000..4ddb2561f --- /dev/null +++ b/packages/discord/test/listStickerPacks.test.ts @@ -0,0 +1,121 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as Redacted from "effect/Redacted"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { + Credentials, + CredentialsFromEnv, + DEFAULT_API_BASE_URL, +} from "../src/credentials.ts"; +import { listStickerPacks } from "../src/operations/listStickerPacks.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /sticker-packs returns the list of Discord-provided sticker packs. +// Parameterless endpoint; a valid bot token is sufficient. Error tests use +// the credentials override pattern with a bogus bearer token to drive auth +// failures independent of operator setup. +const makeInvalidBearerLayer = (token: string): Layer.Layer => + Layer.succeed(Credentials, { + token: Redacted.make(token), + authScheme: "Bearer" as const, + apiBaseUrl: DEFAULT_API_BASE_URL, + }); + +const runWithInvalidBearer = ( + token: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + effect: Effect.Effect, +): Promise => { + const layer = Layer.merge( + makeInvalidBearerLayer(token), + FetchHttpClient.layer, + ); + return Effect.runPromise( + effect.pipe(Effect.provide(layer)) as Effect.Effect, + ); +}; + +describe("listStickerPacks", () => { + it( + "happy path - lists Discord-provided sticker packs", + async () => { + const result = await runEffect(listStickerPacks({})); + expect(Array.isArray(result.sticker_packs)).toBe(true); + for (const pack of result.sticker_packs) { + expect(typeof pack.id).toBe("string"); + expect(typeof pack.sku_id).toBe("string"); + expect(typeof pack.name).toBe("string"); + expect(pack.description === null || typeof pack.description === "string").toBe(true); + expect(Array.isArray(pack.stickers)).toBe(true); + for (const sticker of pack.stickers) { + expect(typeof sticker.id).toBe("string"); + expect(typeof sticker.name).toBe("string"); + expect(typeof sticker.tags).toBe("string"); + expect(typeof sticker.pack_id).toBe("string"); + expect(typeof sticker.sort_value).toBe("number"); + } + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound or Forbidden for an invalid bearer token", async () => { + // Driving an auth failure against /sticker-packs. Discord may surface + // this as Forbidden, NotFound (to avoid leaking existence), BadRequest, + // or Unauthorized depending on routing. + await runWithInvalidBearer( + `invalid-bearer-${testRunId}`, + listStickerPacks({}).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect([ + "NotFound", + "Forbidden", + "Unauthorized", + "BadRequest", + ]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a malformed bearer token", async () => { + // A second deliberately bogus token framed as a malformed credential. + // Discord may surface this as Forbidden, NotFound, BadRequest, or + // Unauthorized. + await runWithInvalidBearer( + `malformed-token-${testRunId}`, + listStickerPacks({}).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect([ + "Forbidden", + "NotFound", + "Unauthorized", + "BadRequest", + ]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listThreadMembers.test.ts b/packages/discord/test/listThreadMembers.test.ts new file mode 100644 index 000000000..ac543eb02 --- /dev/null +++ b/packages/discord/test/listThreadMembers.test.ts @@ -0,0 +1,92 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { listThreadMembers } from "../src/operations/listThreadMembers.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /channels/{channel_id}/thread-members lists members of a thread. +// Requires the GUILD_MEMBERS privileged intent. Happy path is gated on +// DISCORD_TEST_THREAD_ID since it requires a real thread channel id. +const TEST_THREAD_ID = process.env.DISCORD_TEST_THREAD_ID; + +describe("listThreadMembers", () => { + it.skipIf(!TEST_THREAD_ID)( + "happy path - lists members of a thread", + async () => { + const result = await runEffect( + listThreadMembers({ + channel_id: TEST_THREAD_ID!, + with_member: true, + limit: 10, + }), + ); + expect(Array.isArray(result)).toBe(true); + for (const tm of result) { + expect(typeof tm.id).toBe("string"); + expect(typeof tm.user_id).toBe("string"); + expect(typeof tm.join_timestamp).toBe("string"); + expect(typeof tm.flags).toBe("number"); + if (tm.member !== undefined) { + expect(typeof tm.member.flags).toBe("number"); + expect(typeof tm.member.joined_at).toBe("string"); + expect(typeof tm.member.pending).toBe("boolean"); + expect(typeof tm.member.mute).toBe("boolean"); + expect(typeof tm.member.deaf).toBe("boolean"); + expect(Array.isArray(tm.member.roles)).toBe(true); + expect(typeof tm.member.user.id).toBe("string"); + expect(typeof tm.member.user.username).toBe("string"); + } + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent thread channel id", async () => { + const fakeChannelId = `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + listThreadMembers({ channel_id: fakeChannelId }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (thread does not exist), Forbidden + // (bot cannot see the thread), or BadRequest depending on routing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for an inaccessible thread", async () => { + // A snowflake the bot is unlikely to have access to. Discord may surface + // this as Forbidden, NotFound (to avoid leaking existence), or BadRequest. + const inaccessibleChannelId = "100000000000000001"; + await runEffect( + listThreadMembers({ channel_id: inaccessibleChannelId }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/listVoiceRegions.test.ts b/packages/discord/test/listVoiceRegions.test.ts new file mode 100644 index 000000000..5738c64d9 --- /dev/null +++ b/packages/discord/test/listVoiceRegions.test.ts @@ -0,0 +1,115 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as Redacted from "effect/Redacted"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { + Credentials, + CredentialsFromEnv, + DEFAULT_API_BASE_URL, +} from "../src/credentials.ts"; +import { listVoiceRegions } from "../src/operations/listVoiceRegions.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /voice/regions returns a list of available voice regions. Parameterless +// endpoint; a valid bot token is sufficient. Error tests use the credentials +// override pattern with a bogus bearer token to drive auth failures +// independent of operator setup. +const makeInvalidBearerLayer = (token: string): Layer.Layer => + Layer.succeed(Credentials, { + token: Redacted.make(token), + authScheme: "Bearer" as const, + apiBaseUrl: DEFAULT_API_BASE_URL, + }); + +const runWithInvalidBearer = ( + token: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + effect: Effect.Effect, +): Promise => { + const layer = Layer.merge( + makeInvalidBearerLayer(token), + FetchHttpClient.layer, + ); + return Effect.runPromise( + effect.pipe(Effect.provide(layer)) as Effect.Effect, + ); +}; + +describe("listVoiceRegions", () => { + it( + "happy path - lists available voice regions", + async () => { + const result = await runEffect(listVoiceRegions({})); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBeGreaterThan(0); + for (const region of result) { + expect(typeof region.id).toBe("string"); + expect(typeof region.name).toBe("string"); + expect(typeof region.custom).toBe("boolean"); + expect(typeof region.deprecated).toBe("boolean"); + expect(typeof region.optimal).toBe("boolean"); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound or Forbidden for an invalid bearer token", async () => { + // Driving an auth failure against /voice/regions. Discord may surface + // this as Forbidden, NotFound (to avoid leaking existence), BadRequest, + // or Unauthorized depending on routing. + await runWithInvalidBearer( + `invalid-bearer-${testRunId}`, + listVoiceRegions({}).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect([ + "NotFound", + "Forbidden", + "Unauthorized", + "BadRequest", + ]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for a malformed bearer token", async () => { + // A second deliberately bogus token framed as a malformed credential. + // Discord may surface this as Forbidden, NotFound, BadRequest, or + // Unauthorized. + await runWithInvalidBearer( + `malformed-token-${testRunId}`, + listVoiceRegions({}).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect([ + "Forbidden", + "NotFound", + "Unauthorized", + "BadRequest", + ]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/partnerSdkToken.test.ts b/packages/discord/test/partnerSdkToken.test.ts new file mode 100644 index 000000000..5200a3851 --- /dev/null +++ b/packages/discord/test/partnerSdkToken.test.ts @@ -0,0 +1,125 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { partnerSdkToken } from "../src/operations/partnerSdkToken.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// POST /partner-sdk/token exchanges an external auth token for a Discord +// access token via the Discord Partner SDK OAuth2 flow. Requires Discord to +// have approved the application as a Partner SDK integration. The happy path +// is gated on operator-supplied credentials since most apps cannot exercise +// this endpoint. +const PARTNER_CLIENT_ID = process.env.DISCORD_PARTNER_CLIENT_ID; +const PARTNER_CLIENT_SECRET = process.env.DISCORD_PARTNER_CLIENT_SECRET; +const PARTNER_EXTERNAL_AUTH_TOKEN = + process.env.DISCORD_PARTNER_EXTERNAL_AUTH_TOKEN; +const PARTNER_EXTERNAL_AUTH_TYPE = + process.env.DISCORD_PARTNER_EXTERNAL_AUTH_TYPE; + +describe("partnerSdkToken", () => { + it.skipIf( + !PARTNER_CLIENT_ID || + !PARTNER_EXTERNAL_AUTH_TOKEN || + !PARTNER_EXTERNAL_AUTH_TYPE, + )( + "happy path - exchanges an external auth token for a Discord access token", + async () => { + const result = await runEffect( + partnerSdkToken({ + client_id: PARTNER_CLIENT_ID!, + client_secret: PARTNER_CLIENT_SECRET, + external_auth_token: PARTNER_EXTERNAL_AUTH_TOKEN!, + external_auth_type: PARTNER_EXTERNAL_AUTH_TYPE!, + }), + ); + expect(typeof result.token_type).toBe("string"); + expect(result.access_token).toBeDefined(); + expect(typeof result.expires_in).toBe("number"); + expect(typeof result.scope).toBe("string"); + expect(typeof result.id_token).toBe("string"); + }, + { timeout: 30_000 }, + ); + + it("error - BadRequest for an invalid external auth token", async () => { + // A bogus external auth token should fail validation. Discord typically + // surfaces this as BadRequest, but may route as Forbidden or NotFound + // depending on how the partner integration is resolved. + await runEffect( + partnerSdkToken({ + client_id: PARTNER_CLIENT_ID ?? "100000000000000001", + external_auth_token: `bogus-external-token-${testRunId}`, + external_auth_type: PARTNER_EXTERNAL_AUTH_TYPE ?? "epic_online_services_access_token", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for a client_id not approved as a Partner SDK integration", async () => { + // A snowflake that almost certainly does not correspond to an approved + // Partner SDK integration. Discord may surface this as Forbidden, + // BadRequest, or NotFound (to avoid leaking existence) depending on + // routing. + await runEffect( + partnerSdkToken({ + client_id: `1000000000000000${testRunId.slice(0, 2)}`, + external_auth_token: `unauthorized-token-${testRunId}`, + external_auth_type: + PARTNER_EXTERNAL_AUTH_TYPE ?? "epic_online_services_access_token", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "BadRequest", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for a non-existent client_id", async () => { + // A bogus client_id that should not resolve to any Discord application. + // Discord may surface this as NotFound, BadRequest, or Forbidden. + const fakeClientId = "100000000000000001"; + await runEffect( + partnerSdkToken({ + client_id: fakeClientId, + external_auth_token: `nonexistent-${testRunId}`, + external_auth_type: + PARTNER_EXTERNAL_AUTH_TYPE ?? "epic_online_services_access_token", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "BadRequest", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/partnerSdkUnmergeProvisionalAccount.test.ts b/packages/discord/test/partnerSdkUnmergeProvisionalAccount.test.ts new file mode 100644 index 000000000..84d3cbe22 --- /dev/null +++ b/packages/discord/test/partnerSdkUnmergeProvisionalAccount.test.ts @@ -0,0 +1,122 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { partnerSdkUnmergeProvisionalAccount } from "../src/operations/partnerSdkUnmergeProvisionalAccount.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// POST /partner-sdk/provisional-accounts/unmerge unmerges a provisional +// account from a real Discord account via the Discord Partner SDK flow. +// Requires Discord to have approved the application as a Partner SDK +// integration. The happy path is gated on operator-supplied credentials. +const PARTNER_CLIENT_ID = process.env.DISCORD_PARTNER_CLIENT_ID; +const PARTNER_CLIENT_SECRET = process.env.DISCORD_PARTNER_CLIENT_SECRET; +const PARTNER_EXTERNAL_AUTH_TOKEN = + process.env.DISCORD_PARTNER_EXTERNAL_AUTH_TOKEN; +const PARTNER_EXTERNAL_AUTH_TYPE = + process.env.DISCORD_PARTNER_EXTERNAL_AUTH_TYPE; + +describe("partnerSdkUnmergeProvisionalAccount", () => { + it.skipIf( + !PARTNER_CLIENT_ID || + !PARTNER_EXTERNAL_AUTH_TOKEN || + !PARTNER_EXTERNAL_AUTH_TYPE, + )( + "happy path - unmerges a provisional account from a Discord account", + async () => { + const result = await runEffect( + partnerSdkUnmergeProvisionalAccount({ + client_id: PARTNER_CLIENT_ID!, + client_secret: PARTNER_CLIENT_SECRET, + external_auth_token: PARTNER_EXTERNAL_AUTH_TOKEN!, + external_auth_type: PARTNER_EXTERNAL_AUTH_TYPE!, + }), + ); + // The endpoint returns 204 No Content; the typed output is void. + expect(result).toBeUndefined(); + }, + { timeout: 30_000 }, + ); + + it("error - BadRequest for an invalid external auth token", async () => { + // A bogus external auth token should fail validation. Discord typically + // surfaces this as BadRequest, but may route as Forbidden or NotFound + // depending on how the partner integration is resolved. + await runEffect( + partnerSdkUnmergeProvisionalAccount({ + client_id: PARTNER_CLIENT_ID ?? "100000000000000001", + external_auth_token: `bogus-external-token-${testRunId}`, + external_auth_type: + PARTNER_EXTERNAL_AUTH_TYPE ?? "epic_online_services_access_token", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for a client_id not approved as a Partner SDK integration", async () => { + // A snowflake that almost certainly does not correspond to an approved + // Partner SDK integration. Discord may surface this as Forbidden, + // BadRequest, or NotFound (to avoid leaking existence) depending on + // routing. + await runEffect( + partnerSdkUnmergeProvisionalAccount({ + client_id: `1000000000000000${testRunId.slice(0, 2)}`, + external_auth_token: `unauthorized-token-${testRunId}`, + external_auth_type: + PARTNER_EXTERNAL_AUTH_TYPE ?? "epic_online_services_access_token", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "BadRequest", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for a non-existent client_id", async () => { + // A bogus client_id that should not resolve to any Discord application. + // Discord may surface this as NotFound, BadRequest, or Forbidden. + const fakeClientId = "100000000000000001"; + await runEffect( + partnerSdkUnmergeProvisionalAccount({ + client_id: fakeClientId, + external_auth_token: `nonexistent-${testRunId}`, + external_auth_type: + PARTNER_EXTERNAL_AUTH_TYPE ?? "epic_online_services_access_token", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "BadRequest", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/pollExpire.test.ts b/packages/discord/test/pollExpire.test.ts new file mode 100644 index 000000000..167d6d864 --- /dev/null +++ b/packages/discord/test/pollExpire.test.ts @@ -0,0 +1,165 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { createMessage } from "../src/operations/createMessage.ts"; +import { deleteMessage } from "../src/operations/deleteMessage.ts"; +import { pollExpire } from "../src/operations/pollExpire.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// POST /channels/{channel_id}/polls/{message_id}/expire ends a poll early. +// The bot must own the poll. Happy path is gated on DISCORD_TEST_CHANNEL_ID; +// the test creates a fresh poll, expires it, and deletes the message in +// cleanup. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +describe("pollExpire", () => { + it.skipIf(!TEST_CHANNEL_ID)( + "happy path - expires a bot-owned poll early", + async () => { + const created = await runEffect( + createMessage({ + channel_id: TEST_CHANNEL_ID!, + content: `pollExpire test ${testRunId}`, + poll: { + question: { text: `Test poll ${testRunId}` }, + answers: [ + { poll_media: { text: "Yes" } }, + { poll_media: { text: "No" } }, + ], + duration: 1, + allow_multiselect: false, + layout_type: 1, + }, + }), + ); + try { + const result = await runEffect( + pollExpire({ + channel_id: TEST_CHANNEL_ID!, + message_id: created.id, + }), + ); + expect(typeof result.id).toBe("string"); + expect(result.id).toBe(created.id); + expect(typeof result.channel_id).toBe("string"); + expect(result.channel_id).toBe(TEST_CHANNEL_ID); + expect(typeof result.timestamp).toBe("string"); + expect(typeof result.author.id).toBe("string"); + expect(typeof result.flags).toBe("number"); + if (result.poll !== undefined) { + expect(typeof result.poll.expiry).toBe("string"); + expect(typeof result.poll.allow_multiselect).toBe("boolean"); + expect(Array.isArray(result.poll.answers)).toBe(true); + expect(typeof result.poll.results.is_finalized).toBe("boolean"); + } + } finally { + await runEffect( + deleteMessage({ + channel_id: TEST_CHANNEL_ID!, + message_id: created.id, + }).pipe(Effect.ignore), + ); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent poll message", async () => { + const fakeChannelId = + TEST_CHANNEL_ID ?? `1000000000000000${testRunId.slice(0, 2)}`; + const fakeMessageId = `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + pollExpire({ + channel_id: fakeChannelId, + message_id: fakeMessageId, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (message does not exist), Forbidden + // (bot cannot see the channel/message), or BadRequest (message + // exists but has no poll) depending on routing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for an inaccessible channel", async () => { + // A snowflake the bot is unlikely to have access to. Discord may surface + // this as Forbidden, NotFound (to avoid leaking existence), or BadRequest. + const inaccessibleChannelId = "100000000000000001"; + const fakeMessageId = "100000000000000002"; + await runEffect( + pollExpire({ + channel_id: inaccessibleChannelId, + message_id: fakeMessageId, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it.skipIf(!TEST_CHANNEL_ID)( + "error - BadRequest when expiring a non-poll message", + async () => { + // Create a regular message (no poll) and try to expire it. Discord + // typically surfaces this as BadRequest, but may route as NotFound or + // Forbidden. + const created = await runEffect( + createMessage({ + channel_id: TEST_CHANNEL_ID!, + content: `pollExpire-non-poll test ${testRunId}`, + }), + ); + try { + await runEffect( + pollExpire({ + channel_id: TEST_CHANNEL_ID!, + message_id: created.id, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + } finally { + await runEffect( + deleteMessage({ + channel_id: TEST_CHANNEL_ID!, + message_id: created.id, + }).pipe(Effect.ignore), + ); + } + }, + { timeout: 30_000 }, + ); +}); diff --git a/packages/discord/test/previewPruneGuild.test.ts b/packages/discord/test/previewPruneGuild.test.ts new file mode 100644 index 000000000..f892c821a --- /dev/null +++ b/packages/discord/test/previewPruneGuild.test.ts @@ -0,0 +1,80 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { previewPruneGuild } from "../src/operations/previewPruneGuild.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/prune returns the number of members that would be +// pruned by an actual prune. This is read-only — no members are removed. +// Requires KICK_MEMBERS. Happy path is gated on DISCORD_TEST_GUILD_ID. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +describe("previewPruneGuild", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - returns count of members that would be pruned", + async () => { + const result = await runEffect( + previewPruneGuild({ + guild_id: TEST_GUILD_ID!, + days: 7, + }), + ); + expect(result.pruned === null || typeof result.pruned === "number").toBe( + true, + ); + if (result.pruned !== null) { + expect(result.pruned).toBeGreaterThanOrEqual(0); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent guild id", async () => { + const fakeGuildId = `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + previewPruneGuild({ guild_id: fakeGuildId }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (guild does not exist), Forbidden + // (bot is not in the guild), or BadRequest depending on routing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for an inaccessible guild", async () => { + // A snowflake the bot is unlikely to have access to. Discord may surface + // this as Forbidden, NotFound (to avoid leaking existence), or BadRequest. + const inaccessibleGuildId = "100000000000000001"; + await runEffect( + previewPruneGuild({ guild_id: inaccessibleGuildId }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/pruneGuild.test.ts b/packages/discord/test/pruneGuild.test.ts new file mode 100644 index 000000000..122fd138b --- /dev/null +++ b/packages/discord/test/pruneGuild.test.ts @@ -0,0 +1,117 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { pruneGuild } from "../src/operations/pruneGuild.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// POST /guilds/{guild_id}/prune actually prunes inactive members from the +// guild. This is destructive — it kicks members. Happy path is gated on +// DISCORD_TEST_GUILD_ID. Uses days=30 (max) and compute_prune_count=false +// to minimize execution time. Operators must point this at a dedicated test +// guild where pruning is acceptable. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +describe("pruneGuild", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - prunes inactive members and returns count", + async () => { + const result = await runEffect( + pruneGuild({ + guild_id: TEST_GUILD_ID!, + days: 30, + compute_prune_count: false, + }), + ); + // When compute_prune_count is false, Discord returns null. When true, + // it returns the number of pruned members. + expect(result.pruned === null || typeof result.pruned === "number").toBe( + true, + ); + if (typeof result.pruned === "number") { + expect(result.pruned).toBeGreaterThanOrEqual(0); + } + }, + { timeout: 60_000 }, + ); + + it("error - BadRequest for an invalid days value", async () => { + // Discord enforces 1 <= days <= 30 and surfaces out-of-range values as + // BadRequest. May also route as Forbidden or NotFound depending on + // guild access ordering. + const fakeGuildId = + TEST_GUILD_ID ?? `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + pruneGuild({ + guild_id: fakeGuildId, + days: 9999, + compute_prune_count: false, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for an inaccessible guild", async () => { + // A snowflake the bot is unlikely to have access to. Discord may surface + // this as Forbidden, NotFound (to avoid leaking existence), or BadRequest. + const inaccessibleGuildId = "100000000000000001"; + await runEffect( + pruneGuild({ + guild_id: inaccessibleGuildId, + days: 7, + compute_prune_count: false, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for a non-existent guild id", async () => { + const fakeGuildId = `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + pruneGuild({ + guild_id: fakeGuildId, + days: 7, + compute_prune_count: false, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (guild does not exist), Forbidden + // (bot is not in the guild), or BadRequest depending on routing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/putGuildsOnboarding.test.ts b/packages/discord/test/putGuildsOnboarding.test.ts new file mode 100644 index 000000000..0d3d21751 --- /dev/null +++ b/packages/discord/test/putGuildsOnboarding.test.ts @@ -0,0 +1,153 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { getGuildsOnboarding } from "../src/operations/getGuildsOnboarding.ts"; +import { putGuildsOnboarding } from "../src/operations/putGuildsOnboarding.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PUT /guilds/{guild_id}/onboarding replaces the guild's onboarding flow. +// Requires MANAGE_GUILD and MANAGE_ROLES. Happy path is gated on +// DISCORD_TEST_GUILD_ID and snapshots the current onboarding state via +// getGuildsOnboarding before PUTting it back, so the operation is +// effectively idempotent. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +describe("putGuildsOnboarding", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - replaces guild onboarding with the current snapshot", + async () => { + const current = await runEffect( + getGuildsOnboarding({ guild_id: TEST_GUILD_ID! }), + ); + const result = await runEffect( + putGuildsOnboarding({ + guild_id: TEST_GUILD_ID!, + prompts: current.prompts.map((p) => ({ + id: p.id, + title: p.title, + single_select: p.single_select, + required: p.required, + in_onboarding: p.in_onboarding, + type: p.type, + options: p.options.map((o) => ({ + id: o.id, + title: o.title, + description: o.description, + emoji_id: o.emoji.id, + emoji_name: o.emoji.name, + emoji_animated: o.emoji.animated, + role_ids: [...o.role_ids], + channel_ids: [...o.channel_ids], + })), + })), + default_channel_ids: [...current.default_channel_ids], + enabled: current.enabled, + mode: current.mode, + }), + ); + expect(result.guild_id).toBe(TEST_GUILD_ID); + expect(Array.isArray(result.prompts)).toBe(true); + expect(Array.isArray(result.default_channel_ids)).toBe(true); + expect(typeof result.enabled).toBe("boolean"); + for (const prompt of result.prompts) { + expect(typeof prompt.id).toBe("string"); + expect(typeof prompt.title).toBe("string"); + expect(typeof prompt.single_select).toBe("boolean"); + expect(typeof prompt.required).toBe("boolean"); + expect(typeof prompt.in_onboarding).toBe("boolean"); + expect(Array.isArray(prompt.options)).toBe(true); + } + }, + { timeout: 60_000 }, + ); + + it("error - BadRequest for malformed onboarding input", async () => { + // A prompt with an empty options array violates Discord's onboarding + // constraints and should surface as BadRequest. Discord may also route + // as Forbidden or NotFound depending on guild access ordering. + const fakeGuildId = + TEST_GUILD_ID ?? `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + putGuildsOnboarding({ + guild_id: fakeGuildId, + prompts: [ + { + id: "0", + title: `bad-prompt-${testRunId}`, + options: [], + }, + ], + default_channel_ids: [], + enabled: false, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for an inaccessible guild", async () => { + // A snowflake the bot is unlikely to have access to. Discord may surface + // this as Forbidden, NotFound (to avoid leaking existence), or BadRequest. + const inaccessibleGuildId = "100000000000000001"; + await runEffect( + putGuildsOnboarding({ + guild_id: inaccessibleGuildId, + prompts: [], + default_channel_ids: [], + enabled: false, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for a non-existent guild id", async () => { + const fakeGuildId = `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + putGuildsOnboarding({ + guild_id: fakeGuildId, + prompts: [], + default_channel_ids: [], + enabled: false, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (guild does not exist), Forbidden + // (bot is not in the guild), or BadRequest depending on routing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/searchGuildMembers.test.ts b/packages/discord/test/searchGuildMembers.test.ts new file mode 100644 index 000000000..6d7724e01 --- /dev/null +++ b/packages/discord/test/searchGuildMembers.test.ts @@ -0,0 +1,95 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { searchGuildMembers } from "../src/operations/searchGuildMembers.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /guilds/{guild_id}/members/search searches members by username/nickname +// prefix. Requires the GUILD_MEMBERS privileged intent. Happy path is gated +// on DISCORD_TEST_GUILD_ID. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +describe("searchGuildMembers", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - searches guild members by query prefix", + async () => { + const result = await runEffect( + searchGuildMembers({ + guild_id: TEST_GUILD_ID!, + query: "a", + limit: 5, + }), + ); + expect(Array.isArray(result)).toBe(true); + for (const member of result) { + expect(typeof member.flags).toBe("number"); + expect(typeof member.joined_at).toBe("string"); + expect(typeof member.pending).toBe("boolean"); + expect(typeof member.mute).toBe("boolean"); + expect(typeof member.deaf).toBe("boolean"); + expect(Array.isArray(member.roles)).toBe(true); + expect(typeof member.user.id).toBe("string"); + expect(typeof member.user.username).toBe("string"); + expect(typeof member.user.discriminator).toBe("string"); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent guild id", async () => { + const fakeGuildId = `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + searchGuildMembers({ + guild_id: fakeGuildId, + query: `nonexistent-${testRunId}`, + limit: 1, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (guild does not exist), Forbidden + // (bot is not in the guild), or BadRequest depending on routing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for an inaccessible guild", async () => { + // A snowflake the bot is unlikely to have access to. Discord may surface + // this as Forbidden, NotFound (to avoid leaking existence), or BadRequest. + const inaccessibleGuildId = "100000000000000001"; + await runEffect( + searchGuildMembers({ + guild_id: inaccessibleGuildId, + query: `nonexistent-${testRunId}`, + limit: 1, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/sendSoundboardSound.test.ts b/packages/discord/test/sendSoundboardSound.test.ts new file mode 100644 index 000000000..77f037cc4 --- /dev/null +++ b/packages/discord/test/sendSoundboardSound.test.ts @@ -0,0 +1,109 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { sendSoundboardSound } from "../src/operations/sendSoundboardSound.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// POST /channels/{channel_id}/send-soundboard-sound plays a soundboard sound +// in a voice channel. The bot must already be connected to the voice +// channel. Happy path is gated on DISCORD_TEST_VOICE_CHANNEL_ID and +// DISCORD_TEST_SOUNDBOARD_SOUND_ID; the operator is responsible for +// connecting the bot to voice before running. +const TEST_VOICE_CHANNEL_ID = process.env.DISCORD_TEST_VOICE_CHANNEL_ID; +const TEST_SOUNDBOARD_SOUND_ID = process.env.DISCORD_TEST_SOUNDBOARD_SOUND_ID; +const TEST_SOURCE_GUILD_ID = process.env.DISCORD_TEST_SOURCE_GUILD_ID; + +describe("sendSoundboardSound", () => { + it.skipIf(!TEST_VOICE_CHANNEL_ID || !TEST_SOUNDBOARD_SOUND_ID)( + "happy path - plays a soundboard sound in a voice channel", + async () => { + const result = await runEffect( + sendSoundboardSound({ + channel_id: TEST_VOICE_CHANNEL_ID!, + sound_id: TEST_SOUNDBOARD_SOUND_ID!, + source_guild_id: TEST_SOURCE_GUILD_ID, + }), + ); + // Endpoint returns 204 No Content; typed output is void. + expect(result).toBeUndefined(); + }, + { timeout: 30_000 }, + ); + + it("error - BadRequest when the bot is not connected to voice", async () => { + // Without a voice connection, Discord rejects the request. May surface + // as BadRequest, Forbidden, or NotFound depending on routing. + const channelId = + TEST_VOICE_CHANNEL_ID ?? `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + sendSoundboardSound({ + channel_id: channelId, + sound_id: TEST_SOUNDBOARD_SOUND_ID ?? "100000000000000001", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for an inaccessible channel", async () => { + // A snowflake the bot is unlikely to have access to. Discord may surface + // this as Forbidden, NotFound (to avoid leaking existence), or BadRequest. + const inaccessibleChannelId = "100000000000000001"; + await runEffect( + sendSoundboardSound({ + channel_id: inaccessibleChannelId, + sound_id: "100000000000000002", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for a non-existent channel id", async () => { + const fakeChannelId = `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + sendSoundboardSound({ + channel_id: fakeChannelId, + sound_id: `1000000000000000${testRunId.slice(2, 4)}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (channel does not exist), Forbidden + // (bot cannot see the channel), or BadRequest depending on routing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/setChannelPermissionOverwrite.test.ts b/packages/discord/test/setChannelPermissionOverwrite.test.ts new file mode 100644 index 000000000..13ea10e71 --- /dev/null +++ b/packages/discord/test/setChannelPermissionOverwrite.test.ts @@ -0,0 +1,135 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { deleteChannelPermissionOverwrite } from "../src/operations/deleteChannelPermissionOverwrite.ts"; +import { setChannelPermissionOverwrite } from "../src/operations/setChannelPermissionOverwrite.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PUT /channels/{channel_id}/permissions/{overwrite_id} sets a permission +// overwrite on a channel for a specific role or member. Requires +// MANAGE_ROLES. Happy path is gated on DISCORD_TEST_CHANNEL_ID and +// DISCORD_TEST_GUILD_ID — the @everyone role id equals the guild id, and a +// no-op overwrite (allow=0, deny=0) keeps effective permissions unchanged. +// The test cleans up by deleting the overwrite afterwards. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +describe("setChannelPermissionOverwrite", () => { + it.skipIf(!TEST_CHANNEL_ID || !TEST_GUILD_ID)( + "happy path - sets a no-op overwrite for @everyone and cleans up", + async () => { + // type 0 = role overwrite. The @everyone role id equals the guild id. + try { + const result = await runEffect( + setChannelPermissionOverwrite({ + channel_id: TEST_CHANNEL_ID!, + overwrite_id: TEST_GUILD_ID!, + type: 0, + allow: 0, + deny: 0, + }), + ); + // Endpoint returns 204 No Content; typed output is void. + expect(result).toBeUndefined(); + } finally { + await runEffect( + deleteChannelPermissionOverwrite({ + channel_id: TEST_CHANNEL_ID!, + overwrite_id: TEST_GUILD_ID!, + }).pipe(Effect.ignore), + ); + } + }, + { timeout: 30_000 }, + ); + + it("error - BadRequest for an invalid overwrite type", async () => { + // Discord enforces type ∈ {0 (role), 1 (member)}. An out-of-range value + // should surface as BadRequest, but may route as Forbidden or NotFound + // depending on channel access ordering. + const channelId = + TEST_CHANNEL_ID ?? `1000000000000000${testRunId.slice(0, 2)}`; + const overwriteId = + TEST_GUILD_ID ?? `1000000000000000${testRunId.slice(2, 4)}`; + await runEffect( + setChannelPermissionOverwrite({ + channel_id: channelId, + overwrite_id: overwriteId, + type: 9999, + allow: 0, + deny: 0, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for an inaccessible channel", async () => { + // A snowflake the bot is unlikely to have access to. Discord may surface + // this as Forbidden, NotFound (to avoid leaking existence), or BadRequest. + const inaccessibleChannelId = "100000000000000001"; + await runEffect( + setChannelPermissionOverwrite({ + channel_id: inaccessibleChannelId, + overwrite_id: "100000000000000002", + type: 0, + allow: 0, + deny: 0, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for a non-existent channel id", async () => { + const fakeChannelId = `1000000000000000${testRunId.slice(0, 2)}`; + const fakeOverwriteId = `1000000000000000${testRunId.slice(2, 4)}`; + await runEffect( + setChannelPermissionOverwrite({ + channel_id: fakeChannelId, + overwrite_id: fakeOverwriteId, + type: 0, + allow: 0, + deny: 0, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (channel does not exist), Forbidden + // (bot cannot see the channel), or BadRequest depending on routing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/setGuildApplicationCommandPermissions.test.ts b/packages/discord/test/setGuildApplicationCommandPermissions.test.ts new file mode 100644 index 000000000..6f497d9c6 --- /dev/null +++ b/packages/discord/test/setGuildApplicationCommandPermissions.test.ts @@ -0,0 +1,171 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as Redacted from "effect/Redacted"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { + Credentials, + CredentialsFromEnv, + DEFAULT_API_BASE_URL, +} from "../src/credentials.ts"; +import { setGuildApplicationCommandPermissions } from "../src/operations/setGuildApplicationCommandPermissions.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PUT /applications/{application_id}/guilds/{guild_id}/commands/{command_id}/permissions +// sets the permission overrides for a guild command. This endpoint requires +// a Bearer (user OAuth2) token with the +// applications.commands.permissions.update scope — bot tokens are rejected. +// Happy path is gated on DISCORD_BEARER_TOKEN, DISCORD_TEST_APPLICATION_ID, +// DISCORD_TEST_GUILD_ID, and DISCORD_TEST_GUILD_COMMAND_ID. +const TEST_BEARER = process.env.DISCORD_BEARER_TOKEN; +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_GUILD_COMMAND_ID = process.env.DISCORD_TEST_GUILD_COMMAND_ID; + +const makeBearerLayer = (token: string): Layer.Layer => + Layer.succeed(Credentials, { + token: Redacted.make(token), + authScheme: "Bearer" as const, + apiBaseUrl: DEFAULT_API_BASE_URL, + }); + +const runWithBearer = ( + token: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + effect: Effect.Effect, +): Promise => { + const layer = Layer.merge(makeBearerLayer(token), FetchHttpClient.layer); + return Effect.runPromise( + effect.pipe(Effect.provide(layer)) as Effect.Effect, + ); +}; + +describe("setGuildApplicationCommandPermissions", () => { + it.skipIf( + !TEST_BEARER || + !TEST_APPLICATION_ID || + !TEST_GUILD_ID || + !TEST_GUILD_COMMAND_ID, + )( + "happy path - clears permission overrides for a guild command", + async () => { + const result = await runWithBearer( + TEST_BEARER!, + setGuildApplicationCommandPermissions({ + application_id: TEST_APPLICATION_ID!, + guild_id: TEST_GUILD_ID!, + command_id: TEST_GUILD_COMMAND_ID!, + permissions: [], + }), + ); + expect(typeof result.id).toBe("string"); + expect(result.id).toBe(TEST_GUILD_COMMAND_ID); + expect(typeof result.application_id).toBe("string"); + expect(result.application_id).toBe(TEST_APPLICATION_ID); + expect(typeof result.guild_id).toBe("string"); + expect(result.guild_id).toBe(TEST_GUILD_ID); + expect(Array.isArray(result.permissions)).toBe(true); + for (const p of result.permissions) { + expect(typeof p.id).toBe("string"); + expect(typeof p.permission).toBe("boolean"); + } + }, + { timeout: 30_000 }, + ); + + it("error - BadRequest for malformed permissions input", async () => { + // type ∈ {1 (role), 2 (user), 3 (channel)}. An out-of-range value should + // surface as BadRequest. May also route as Forbidden or NotFound + // depending on resource access ordering. + const fakeAppId = + TEST_APPLICATION_ID ?? `1000000000000000${testRunId.slice(0, 2)}`; + const fakeGuildId = + TEST_GUILD_ID ?? `1000000000000000${testRunId.slice(2, 4)}`; + const fakeCommandId = + TEST_GUILD_COMMAND_ID ?? `1000000000000000${testRunId.slice(4, 6)}`; + await runEffect( + setGuildApplicationCommandPermissions({ + application_id: fakeAppId, + guild_id: fakeGuildId, + command_id: fakeCommandId, + permissions: [ + { + id: "100000000000000001", + type: 9999, + permission: true, + }, + ], + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for an inaccessible application/guild", async () => { + // A snowflake set the bot/user is unlikely to have access to. Discord + // may surface this as Forbidden, NotFound (to avoid leaking existence), + // or BadRequest. + await runEffect( + setGuildApplicationCommandPermissions({ + application_id: "100000000000000001", + guild_id: "100000000000000002", + command_id: "100000000000000003", + permissions: [], + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for a non-existent command id", async () => { + const appId = + TEST_APPLICATION_ID ?? `1000000000000000${testRunId.slice(0, 2)}`; + const guildId = + TEST_GUILD_ID ?? `1000000000000000${testRunId.slice(2, 4)}`; + const fakeCommandId = `1000000000000000${testRunId.slice(4, 6)}`; + await runEffect( + setGuildApplicationCommandPermissions({ + application_id: appId, + guild_id: guildId, + command_id: fakeCommandId, + permissions: [], + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (command does not exist), Forbidden + // (caller cannot see it), or BadRequest depending on routing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/syncGuildTemplate.test.ts b/packages/discord/test/syncGuildTemplate.test.ts new file mode 100644 index 000000000..dbb2192e3 --- /dev/null +++ b/packages/discord/test/syncGuildTemplate.test.ts @@ -0,0 +1,143 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { createGuildTemplate } from "../src/operations/createGuildTemplate.ts"; +import { deleteGuildTemplate } from "../src/operations/deleteGuildTemplate.ts"; +import { syncGuildTemplate } from "../src/operations/syncGuildTemplate.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PUT /guilds/{guild_id}/templates/{code} re-syncs an existing guild template +// to the current guild state. Requires MANAGE_GUILD. Happy path is gated on +// DISCORD_TEST_GUILD_ID; the test creates a fresh template, syncs it, then +// deletes it. Discord allows only one template per guild — operators must +// ensure the test guild has no existing template before running. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +describe("syncGuildTemplate", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - syncs an existing template to the current guild state", + async () => { + const created = await runEffect( + createGuildTemplate({ + guild_id: TEST_GUILD_ID!, + name: `sync-test-${testRunId}`, + description: "syncGuildTemplate test", + }), + ); + try { + const result = await runEffect( + syncGuildTemplate({ + guild_id: TEST_GUILD_ID!, + code: created.code, + }), + ); + expect(result.code).toBe(created.code); + expect(result.source_guild_id).toBe(TEST_GUILD_ID); + expect(typeof result.name).toBe("string"); + expect(typeof result.usage_count).toBe("number"); + expect(typeof result.creator_id).toBe("string"); + expect(typeof result.created_at).toBe("string"); + expect(typeof result.updated_at).toBe("string"); + expect(typeof result.serialized_source_guild.name).toBe("string"); + expect(typeof result.serialized_source_guild.system_channel_flags).toBe( + "number", + ); + expect(Array.isArray(result.serialized_source_guild.roles)).toBe(true); + expect(Array.isArray(result.serialized_source_guild.channels)).toBe( + true, + ); + expect( + result.is_dirty === null || typeof result.is_dirty === "boolean", + ).toBe(true); + } finally { + await runEffect( + deleteGuildTemplate({ + guild_id: TEST_GUILD_ID!, + code: created.code, + }).pipe(Effect.ignore), + ); + } + }, + { timeout: 60_000 }, + ); + + it("error - BadRequest for a malformed template code", async () => { + // An empty / invalid template code should surface as BadRequest. Discord + // may also route as Forbidden or NotFound depending on guild access + // ordering. + const guildId = + TEST_GUILD_ID ?? `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + syncGuildTemplate({ + guild_id: guildId, + code: " ", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for an inaccessible guild", async () => { + // A snowflake the bot is unlikely to have access to. Discord may surface + // this as Forbidden, NotFound (to avoid leaking existence), or BadRequest. + const inaccessibleGuildId = "100000000000000001"; + await runEffect( + syncGuildTemplate({ + guild_id: inaccessibleGuildId, + code: `bogus-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for a non-existent template code", async () => { + const guildId = + TEST_GUILD_ID ?? `1000000000000000${testRunId.slice(0, 2)}`; + const fakeCode = `nonexistent-template-${testRunId}`; + await runEffect( + syncGuildTemplate({ + guild_id: guildId, + code: fakeCode, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (template does not exist), Forbidden + // (bot is not in the guild), or BadRequest depending on routing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/threadSearch.test.ts b/packages/discord/test/threadSearch.test.ts new file mode 100644 index 000000000..c1837e1bc --- /dev/null +++ b/packages/discord/test/threadSearch.test.ts @@ -0,0 +1,88 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { threadSearch } from "../src/operations/threadSearch.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// GET /channels/{channel_id}/threads/search searches threads in a forum or +// media channel. Requires READ_MESSAGE_HISTORY. Happy path is gated on +// DISCORD_TEST_FORUM_CHANNEL_ID — must be a forum/media channel. +const TEST_FORUM_CHANNEL_ID = process.env.DISCORD_TEST_FORUM_CHANNEL_ID; + +describe("threadSearch", () => { + it.skipIf(!TEST_FORUM_CHANNEL_ID)( + "happy path - searches threads in a forum channel", + async () => { + const result = await runEffect( + threadSearch({ + channel_id: TEST_FORUM_CHANNEL_ID!, + limit: 5, + }), + ); + expect(Array.isArray(result.threads)).toBe(true); + for (const thread of result.threads) { + expect(typeof thread.id).toBe("string"); + expect(typeof thread.guild_id).toBe("string"); + expect(typeof thread.name).toBe("string"); + expect(typeof thread.owner_id).toBe("string"); + expect(typeof thread.flags).toBe("number"); + expect(typeof thread.message_count).toBe("number"); + expect(typeof thread.member_count).toBe("number"); + expect(typeof thread.total_message_sent).toBe("number"); + expect(typeof thread.thread_metadata.archived).toBe("boolean"); + expect(typeof thread.thread_metadata.locked).toBe("boolean"); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent channel id", async () => { + const fakeChannelId = `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + threadSearch({ channel_id: fakeChannelId, limit: 1 }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (channel does not exist), Forbidden + // (bot cannot see the channel), or BadRequest (channel is not a + // forum/media channel) depending on routing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for an inaccessible channel", async () => { + // A snowflake the bot is unlikely to have access to. Discord may surface + // this as Forbidden, NotFound (to avoid leaking existence), or BadRequest. + const inaccessibleChannelId = "100000000000000001"; + await runEffect( + threadSearch({ channel_id: inaccessibleChannelId, limit: 1 }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/triggerTypingIndicator.test.ts b/packages/discord/test/triggerTypingIndicator.test.ts new file mode 100644 index 000000000..c72df8db8 --- /dev/null +++ b/packages/discord/test/triggerTypingIndicator.test.ts @@ -0,0 +1,92 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { triggerTypingIndicator } from "../src/operations/triggerTypingIndicator.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// POST /channels/{channel_id}/typing posts a "typing..." indicator that +// auto-expires after ~10 seconds. Requires SEND_MESSAGES on the channel. +// Happy path is gated on DISCORD_TEST_CHANNEL_ID. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +describe("triggerTypingIndicator", () => { + it.skipIf(!TEST_CHANNEL_ID)( + "happy path - posts a typing indicator in a channel", + async () => { + const result = await runEffect( + triggerTypingIndicator({ channel_id: TEST_CHANNEL_ID! }), + ); + // Endpoint returns 204 No Content; the typed output is an empty struct. + expect(typeof result).toBe("object"); + }, + { timeout: 30_000 }, + ); + + it("error - BadRequest for a malformed channel id", async () => { + // A non-snowflake channel id should fail validation. Discord may surface + // this as BadRequest, NotFound, or Forbidden depending on routing. + await runEffect( + triggerTypingIndicator({ + channel_id: `not-a-snowflake-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for an inaccessible channel", async () => { + // A snowflake the bot is unlikely to have access to. Discord may surface + // this as Forbidden, NotFound (to avoid leaking existence), or BadRequest. + const inaccessibleChannelId = "100000000000000001"; + await runEffect( + triggerTypingIndicator({ channel_id: inaccessibleChannelId }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for a non-existent channel id", async () => { + const fakeChannelId = `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + triggerTypingIndicator({ channel_id: fakeChannelId }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (channel does not exist), Forbidden + // (bot cannot see the channel), or BadRequest depending on routing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/unbanUserFromGuild.test.ts b/packages/discord/test/unbanUserFromGuild.test.ts new file mode 100644 index 000000000..505b74b90 --- /dev/null +++ b/packages/discord/test/unbanUserFromGuild.test.ts @@ -0,0 +1,110 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { banUserFromGuild } from "../src/operations/banUserFromGuild.ts"; +import { unbanUserFromGuild } from "../src/operations/unbanUserFromGuild.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// DELETE /guilds/{guild_id}/bans/{user_id} removes a guild ban. Requires +// BAN_MEMBERS. Happy path is gated on DISCORD_TEST_GUILD_ID; the test bans +// a synthetic user snowflake first and then unbans it. Discord accepts any +// well-formed snowflake for the ban list, so no real user is affected. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// A synthetic but well-formed snowflake derived from testRunId so it doesn't +// collide with another concurrent run. Discord snowflakes are 17-19 digit +// numbers; 17 digits keeps us comfortably within range. +const syntheticUserId = `2000000000000${testRunId.slice(0, 4)}`; + +describe("unbanUserFromGuild", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - unbans a previously banned synthetic user", + async () => { + await runEffect( + banUserFromGuild({ + guild_id: TEST_GUILD_ID!, + user_id: syntheticUserId, + }), + ); + try { + const result = await runEffect( + unbanUserFromGuild({ + guild_id: TEST_GUILD_ID!, + user_id: syntheticUserId, + }), + ); + // Endpoint returns 204 No Content; the typed output is void. + expect(result).toBeUndefined(); + } finally { + // Defensive cleanup in case the unban above failed midway. + await runEffect( + unbanUserFromGuild({ + guild_id: TEST_GUILD_ID!, + user_id: syntheticUserId, + }).pipe(Effect.ignore), + ); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound when the user is not banned", async () => { + // A user_id that has never been banned in the test guild. Discord + // typically surfaces this as NotFound, but may route as Forbidden + // (insufficient access) or BadRequest depending on guild access + // ordering. + const guildId = + TEST_GUILD_ID ?? `1000000000000000${testRunId.slice(0, 2)}`; + const neverBannedUserId = `3000000000000${testRunId.slice(4, 8)}`; + await runEffect( + unbanUserFromGuild({ + guild_id: guildId, + user_id: neverBannedUserId, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for an inaccessible guild", async () => { + // A guild snowflake the bot is unlikely to have access to. Discord may + // surface this as Forbidden, NotFound (to avoid leaking existence), or + // BadRequest. + const inaccessibleGuildId = "100000000000000001"; + await runEffect( + unbanUserFromGuild({ + guild_id: inaccessibleGuildId, + user_id: "100000000000000002", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateApplication.test.ts b/packages/discord/test/updateApplication.test.ts new file mode 100644 index 000000000..b06865148 --- /dev/null +++ b/packages/discord/test/updateApplication.test.ts @@ -0,0 +1,109 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { getMyApplication } from "../src/operations/getMyApplication.ts"; +import { updateApplication } from "../src/operations/updateApplication.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PATCH /applications/{application_id} updates the calling bot's application. +// The bot can only update its own app (use "@me" or the bot's app id). +// Happy path snapshots the current description and PATCHes it back so the +// effective state is unchanged. + +describe("updateApplication", () => { + it( + "happy path - snapshots and re-applies the bot's application description", + async () => { + const me = await runEffect(getMyApplication({})); + const result = await runEffect( + updateApplication({ + application_id: me.id, + description: { default: me.description }, + }), + ); + expect(result.id).toBe(me.id); + expect(typeof result.name).toBe("string"); + expect(typeof result.description).toBe("string"); + expect(result.description).toBe(me.description); + }, + { timeout: 30_000 }, + ); + + it("error - BadRequest for an invalid interactions_endpoint_url", async () => { + // A non-https URL should fail validation. Discord may surface this as + // BadRequest, but may route as Forbidden or NotFound depending on + // application access ordering. + const me = await runEffect(getMyApplication({})); + await runEffect( + updateApplication({ + application_id: me.id, + interactions_endpoint_url: `not-a-valid-url-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for an application the caller does not own", async () => { + // A real-looking snowflake the bot does not own. Discord may surface this + // as Forbidden, NotFound (to avoid leaking existence), or BadRequest. + const inaccessibleApplicationId = "100000000000000001"; + await runEffect( + updateApplication({ + application_id: inaccessibleApplicationId, + description: { default: `forbidden-${testRunId}` }, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for a non-existent application id", async () => { + const fakeApplicationId = `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + updateApplication({ + application_id: fakeApplicationId, + description: { default: `nonexistent-${testRunId}` }, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (application does not exist), + // Forbidden (caller does not own it), or BadRequest depending on + // routing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateApplicationCommand.test.ts b/packages/discord/test/updateApplicationCommand.test.ts new file mode 100644 index 000000000..bd98da4e4 --- /dev/null +++ b/packages/discord/test/updateApplicationCommand.test.ts @@ -0,0 +1,154 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { createApplicationCommand } from "../src/operations/createApplicationCommand.ts"; +import { deleteApplicationCommand } from "../src/operations/deleteApplicationCommand.ts"; +import { getMyApplication } from "../src/operations/getMyApplication.ts"; +import { updateApplicationCommand } from "../src/operations/updateApplicationCommand.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PATCH /applications/{application_id}/commands/{command_id} updates a global +// application command. The bot can only update its own commands. The test +// creates a fresh global command, updates it, and deletes it in cleanup. +// Slash command names must be lowercase 1-32 chars matching ^[-_\p{L}\p{N}]+$. +const commandName = (suffix: string) => + `dist-upd-${suffix}-${testRunId}`.toLowerCase(); + +describe("updateApplicationCommand", () => { + it( + "happy path - updates the description of a global application command", + async () => { + const me = await runEffect(getMyApplication({})); + const created = await runEffect( + createApplicationCommand({ + application_id: me.id, + name: commandName("orig"), + description: `original description ${testRunId}`, + }), + ); + try { + const newDescription = `updated description ${testRunId}`; + const result = await runEffect( + updateApplicationCommand({ + application_id: me.id, + command_id: created.id, + description: newDescription, + }), + ); + expect(result.id).toBe(created.id); + expect(result.application_id).toBe(me.id); + expect(typeof result.name).toBe("string"); + expect(result.description).toBe(newDescription); + expect(typeof result.version).toBe("string"); + } finally { + await runEffect( + deleteApplicationCommand({ + application_id: me.id, + command_id: created.id, + }).pipe(Effect.ignore), + ); + } + }, + { timeout: 60_000 }, + ); + + it("error - BadRequest for an invalid name format", async () => { + // Slash command names must be lowercase and match + // ^[-_\p{L}\p{N}]+$. An uppercase name with spaces should fail + // validation as BadRequest. Discord may also route as Forbidden or + // NotFound depending on access ordering. + const me = await runEffect(getMyApplication({})); + const created = await runEffect( + createApplicationCommand({ + application_id: me.id, + name: commandName("badname"), + description: `bad name test ${testRunId}`, + }), + ); + try { + await runEffect( + updateApplicationCommand({ + application_id: me.id, + command_id: created.id, + name: `Invalid Name With Spaces ${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + } finally { + await runEffect( + deleteApplicationCommand({ + application_id: me.id, + command_id: created.id, + }).pipe(Effect.ignore), + ); + } + }); + + it("error - Forbidden for an application the caller does not own", async () => { + // A real-looking snowflake the bot does not own. Discord may surface + // this as Forbidden, NotFound (to avoid leaking existence), or + // BadRequest. + const inaccessibleApplicationId = "100000000000000001"; + const fakeCommandId = "100000000000000002"; + await runEffect( + updateApplicationCommand({ + application_id: inaccessibleApplicationId, + command_id: fakeCommandId, + description: `forbidden ${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for a non-existent command id", async () => { + const me = await runEffect(getMyApplication({})); + const fakeCommandId = `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + updateApplicationCommand({ + application_id: me.id, + command_id: fakeCommandId, + description: `nonexistent ${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (command does not exist), Forbidden + // (caller cannot see it), or BadRequest depending on routing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateApplicationEmoji.test.ts b/packages/discord/test/updateApplicationEmoji.test.ts new file mode 100644 index 000000000..f3ac8bc8b --- /dev/null +++ b/packages/discord/test/updateApplicationEmoji.test.ts @@ -0,0 +1,170 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createApplicationEmoji } from "../src/operations/createApplicationEmoji.ts"; +import { deleteApplicationEmoji } from "../src/operations/deleteApplicationEmoji.ts"; +import { updateApplicationEmoji } from "../src/operations/updateApplicationEmoji.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Smallest valid 1x1 transparent PNG, encoded as a data URI. +const TINY_PNG_DATA_URI = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII="; + +// Requires the bot's application_id. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; +const NON_EXISTENT_EMOJI_ID = "100000000000000001"; + +// Discord requires emoji names to match ^[a-zA-Z0-9_]{2,32}$. +const emojiName = (suffix: string): string => { + const raw = `dt_${suffix}_${testRunId}`; + return raw.replace(/[^a-zA-Z0-9_]/g, "_").slice(0, 32); +}; + +describe("updateApplicationEmoji", () => { + it("happy path - renames a freshly created application emoji", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the updateApplicationEmoji happy path", + ); + } + const emoji = await runEffect( + createApplicationEmoji({ + application_id: TEST_APPLICATION_ID, + name: emojiName("orig"), + image: TINY_PNG_DATA_URI, + }), + ); + try { + const renamed = emojiName("upd"); + const result = await runEffect( + updateApplicationEmoji({ + application_id: TEST_APPLICATION_ID, + emoji_id: emoji.id, + name: renamed, + }), + ); + expect(result.id).toBe(emoji.id); + expect(result.name).toBe(renamed); + expect(typeof result.require_colons).toBe("boolean"); + expect(typeof result.managed).toBe("boolean"); + expect(typeof result.animated).toBe("boolean"); + expect(typeof result.available).toBe("boolean"); + expect(Array.isArray(result.roles)).toBe(true); + } finally { + await runEffect( + deleteApplicationEmoji({ + application_id: TEST_APPLICATION_ID, + emoji_id: emoji.id, + }).pipe(Effect.ignore), + ); + } + }); + + it("error - BadRequest for an invalid emoji name", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the BadRequest test", + ); + } + // Discord requires names to match ^[a-zA-Z0-9_]{2,32}$. A name with + // disallowed characters should fail validation as BadRequest. Discord + // may also route as Forbidden or NotFound depending on access ordering. + const emoji = await runEffect( + createApplicationEmoji({ + application_id: TEST_APPLICATION_ID, + name: emojiName("badname"), + image: TINY_PNG_DATA_URI, + }), + ); + try { + await runEffect( + updateApplicationEmoji({ + application_id: TEST_APPLICATION_ID, + emoji_id: emoji.id, + name: `bad name with spaces ${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + } finally { + await runEffect( + deleteApplicationEmoji({ + application_id: TEST_APPLICATION_ID, + emoji_id: emoji.id, + }).pipe(Effect.ignore), + ); + } + }); + + it("error - Forbidden when the bot does not own the application_id", async () => { + // A snowflake-shaped application_id the bot's token does not own + // typically yields 403 Forbidden, or 404 NotFound if the route 404s + // before the ownership check, or BadRequest depending on routing. + await runEffect( + updateApplicationEmoji({ + application_id: NON_EXISTENT_APPLICATION_ID, + emoji_id: NON_EXISTENT_EMOJI_ID, + name: emojiName("forbid"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for non-existent emoji_id", async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the NotFound test", + ); + } + // Discord returns 404 NotFound for emoji_ids that do not exist on the + // application. May also surface as Forbidden or BadRequest depending on + // routing. + await runEffect( + updateApplicationEmoji({ + application_id: TEST_APPLICATION_ID, + emoji_id: NON_EXISTENT_EMOJI_ID, + name: emojiName("notfound"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateApplicationRoleConnectionsMetadata.test.ts b/packages/discord/test/updateApplicationRoleConnectionsMetadata.test.ts new file mode 100644 index 000000000..2bc2badad --- /dev/null +++ b/packages/discord/test/updateApplicationRoleConnectionsMetadata.test.ts @@ -0,0 +1,106 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { updateApplicationRoleConnectionsMetadata } from "../src/operations/updateApplicationRoleConnectionsMetadata.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PUT /applications/{application_id}/role-connections/metadata replaces the +// application's role connection metadata records. The SDK's input schema +// has only `application_id`, so calling it sends no body, which Discord +// interprets as clearing all metadata records. +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; + +describe("updateApplicationRoleConnectionsMetadata", () => { + it.skipIf(!TEST_APPLICATION_ID)( + "happy path - replaces role-connection metadata for the bot's application", + async () => { + const result = await runEffect( + updateApplicationRoleConnectionsMetadata({ + application_id: TEST_APPLICATION_ID!, + }), + ); + expect(Array.isArray(result)).toBe(true); + for (const record of result) { + expect(typeof record.key).toBe("string"); + expect(typeof record.name).toBe("string"); + expect(typeof record.description).toBe("string"); + } + }, + { timeout: 30_000 }, + ); + + it("error - BadRequest for a malformed application_id", async () => { + // A non-snowflake application_id should fail validation. Discord may + // surface this as BadRequest, NotFound, or Forbidden depending on + // routing. + await runEffect( + updateApplicationRoleConnectionsMetadata({ + application_id: `not-a-snowflake-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for an application the caller does not own", async () => { + // A real-looking snowflake the bot does not own. Discord may surface + // this as Forbidden, NotFound (to avoid leaking existence), or + // BadRequest. + await runEffect( + updateApplicationRoleConnectionsMetadata({ + application_id: NON_EXISTENT_APPLICATION_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for a non-existent application id", async () => { + const fakeApplicationId = `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + updateApplicationRoleConnectionsMetadata({ + application_id: fakeApplicationId, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (application does not exist), + // Forbidden (caller does not own it), or BadRequest depending on + // routing. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateApplicationUserRoleConnection.test.ts b/packages/discord/test/updateApplicationUserRoleConnection.test.ts new file mode 100644 index 000000000..a475e731d --- /dev/null +++ b/packages/discord/test/updateApplicationUserRoleConnection.test.ts @@ -0,0 +1,158 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as Redacted from "effect/Redacted"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { + Credentials, + CredentialsFromEnv, + DEFAULT_API_BASE_URL, +} from "../src/credentials.ts"; +import { updateApplicationUserRoleConnection } from "../src/operations/updateApplicationUserRoleConnection.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PUT /users/@me/applications/{application_id}/role-connection updates the +// calling user's application role connection. This endpoint is user-only and +// requires a Bearer token with the `role_connections.write` scope; bot +// tokens are rejected. The happy path is gated on DISCORD_BEARER_TOKEN and +// DISCORD_TEST_APPLICATION_ID. +const TEST_BEARER = process.env.DISCORD_BEARER_TOKEN; +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; + +// A deliberately bogus bearer token used for the error tests so they don't +// depend on operator credentials. +const makeBearerLayer = (token: string): Layer.Layer => + Layer.succeed(Credentials, { + token: Redacted.make(token), + authScheme: "Bearer" as const, + apiBaseUrl: DEFAULT_API_BASE_URL, + }); + +const runWithBearer = ( + token: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + effect: Effect.Effect, +): Promise => { + const layer = Layer.merge(makeBearerLayer(token), FetchHttpClient.layer); + return Effect.runPromise( + effect.pipe(Effect.provide(layer)) as Effect.Effect, + ); +}; + +describe("updateApplicationUserRoleConnection", () => { + it.skipIf(!TEST_BEARER || !TEST_APPLICATION_ID)( + "happy path - updates the calling user's application role connection", + async () => { + const result = await runEffect( + updateApplicationUserRoleConnection({ + application_id: TEST_APPLICATION_ID!, + platform_name: `distilled-discord-${testRunId}`, + platform_username: `tester-${testRunId}`, + metadata: {}, + }), + ); + if (result.platform_name !== undefined) { + expect(typeof result.platform_name).toBe("string"); + } + if (result.platform_username !== undefined && result.platform_username !== null) { + expect(typeof result.platform_username).toBe("string"); + } + if (result.metadata !== undefined) { + expect(typeof result.metadata).toBe("object"); + } + }, + { timeout: 30_000 }, + ); + + it("error - BadRequest for a malformed application_id", async () => { + // A non-snowflake application_id should fail validation. Discord may + // surface this as BadRequest, NotFound, Forbidden, or Unauthorized + // depending on routing. + await runWithBearer( + `invalid-bearer-${testRunId}`, + updateApplicationUserRoleConnection({ + application_id: `not-a-snowflake-${testRunId}`, + platform_name: `distilled-discord-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "BadRequest", + "NotFound", + "Forbidden", + "Unauthorized", + ]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for a bearer token missing the role_connections.write scope", async () => { + // A deliberately bogus bearer token framed as missing the required + // scope. /users/@me/applications/{application_id}/role-connection + // requires `role_connections.write`; Discord may surface this as + // Forbidden, Unauthorized, NotFound, or BadRequest. + await runWithBearer( + `no-scope-${testRunId}`, + updateApplicationUserRoleConnection({ + application_id: NON_EXISTENT_APPLICATION_ID, + platform_name: `distilled-discord-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "Forbidden", + "Unauthorized", + "NotFound", + "BadRequest", + ]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for a non-existent application id", async () => { + const fakeApplicationId = `1000000000000000${testRunId.slice(0, 2)}`; + await runWithBearer( + `invalid-bearer-${testRunId}`, + updateApplicationUserRoleConnection({ + application_id: fakeApplicationId, + platform_name: `distilled-discord-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (application does not exist), + // Forbidden (caller cannot access it), Unauthorized (bad token), + // or BadRequest depending on routing. + expect([ + "NotFound", + "Forbidden", + "Unauthorized", + "BadRequest", + ]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateAutoModerationRule.test.ts b/packages/discord/test/updateAutoModerationRule.test.ts new file mode 100644 index 000000000..166333d1d --- /dev/null +++ b/packages/discord/test/updateAutoModerationRule.test.ts @@ -0,0 +1,138 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { updateAutoModerationRule } from "../src/operations/updateAutoModerationRule.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PATCH /guilds/{guild_id}/auto-moderation/rules/{rule_id} updates an +// auto-moderation rule. The SDK input has a codegen gap — only guild_id +// and rule_id are exposed (no body fields) — so calling it sends an empty +// body. Discord may treat that as a no-op (returning the unchanged rule) +// or reject it with BadRequest. The happy path is gated on +// DISCORD_TEST_GUILD_ID + DISCORD_TEST_AUTO_MODERATION_RULE_ID since the +// SDK cannot create a rule (createAutoModerationRule has the same codegen +// gap). +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_AUTO_MODERATION_RULE_ID = + process.env.DISCORD_TEST_AUTO_MODERATION_RULE_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_RULE_ID = "100000000000000001"; + +describe("updateAutoModerationRule", () => { + it.skipIf(!TEST_GUILD_ID || !TEST_AUTO_MODERATION_RULE_ID)( + "happy path - patches a pre-existing auto-moderation rule with an empty body", + async () => { + void testRunId; + // Empty-body PATCH should either echo back the rule or fail with + // BadRequest depending on Discord's validation. We accept either by + // running the effect and inspecting the success/failure shape. + await runEffect( + updateAutoModerationRule({ + guild_id: TEST_GUILD_ID!, + rule_id: TEST_AUTO_MODERATION_RULE_ID!, + }).pipe( + Effect.matchEffect({ + onSuccess: (result) => + Effect.sync(() => { + // Output schema is Unknown; if the API echoed back the rule, + // it should at least be a non-null value. + expect(result === undefined || result !== null).toBe(true); + }), + onFailure: (e) => + Effect.sync(() => { + // Discord may reject empty-body PATCHes as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + }), + ), + ); + }, + { timeout: 30_000 }, + ); + + it("error - BadRequest or NotFound for malformed rule_id", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the BadRequest test", + ); + } + // A non-snowflake rule_id should fail validation. Discord may surface + // this as BadRequest or NotFound depending on routing. + await runEffect( + updateAutoModerationRule({ + guild_id: TEST_GUILD_ID, + rule_id: `not-a-snowflake-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot cannot moderate", async () => { + // A snowflake-shaped guild_id the bot cannot see typically yields 403 + // Forbidden (50001 Missing Access), or 404 NotFound if the route 404s + // before the permission check. + await runEffect( + updateAutoModerationRule({ + guild_id: NON_EXISTENT_GUILD_ID, + rule_id: NON_EXISTENT_RULE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for non-existent rule_id in a real guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + // Discord returns 404 NotFound for rule_ids that do not exist on the guild. + await runEffect( + updateAutoModerationRule({ + guild_id: TEST_GUILD_ID, + rule_id: NON_EXISTENT_RULE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateChannel.test.ts b/packages/discord/test/updateChannel.test.ts new file mode 100644 index 000000000..82be81696 --- /dev/null +++ b/packages/discord/test/updateChannel.test.ts @@ -0,0 +1,107 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { updateChannel } from "../src/operations/updateChannel.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PATCH /channels/{channel_id} updates a channel. The SDK input has a +// codegen gap — only channel_id is exposed (no body fields) — so calling +// it sends an empty body. Discord typically treats empty PATCHes as a +// no-op and returns the unchanged channel. The happy path is gated on +// DISCORD_TEST_CHANNEL_ID. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifier that should not match a real channel. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; + +describe("updateChannel", () => { + it.skipIf(!TEST_CHANNEL_ID)( + "happy path - patches a real channel with an empty body", + async () => { + void testRunId; + const result = await runEffect( + updateChannel({ + channel_id: TEST_CHANNEL_ID!, + }), + ); + // Output schema is Unknown; for an empty-body PATCH Discord echoes + // back the channel object, which should at least be a non-null + // object exposing an id matching the requested channel. + expect(result).not.toBeNull(); + expect(typeof result).toBe("object"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const channel = result as any; + if (channel?.id !== undefined) { + expect(channel.id).toBe(TEST_CHANNEL_ID); + } + }, + { timeout: 30_000 }, + ); + + it("error - BadRequest for a malformed channel_id", async () => { + // A non-snowflake channel_id should fail validation. Discord may + // surface this as BadRequest or NotFound depending on routing. + await runEffect( + updateChannel({ + channel_id: `not-a-snowflake-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a channel the bot cannot access", async () => { + // A snowflake-shaped channel_id the bot cannot see typically yields + // 403 Forbidden (50001 Missing Access), or 404 NotFound if the route + // 404s before the permission check. + await runEffect( + updateChannel({ + channel_id: NON_EXISTENT_CHANNEL_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for a non-existent channel id", async () => { + const fakeChannelId = `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + updateChannel({ + channel_id: fakeChannelId, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateGuild.test.ts b/packages/discord/test/updateGuild.test.ts new file mode 100644 index 000000000..d831d8840 --- /dev/null +++ b/packages/discord/test/updateGuild.test.ts @@ -0,0 +1,113 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getGuild } from "../src/operations/getGuild.ts"; +import { updateGuild } from "../src/operations/updateGuild.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PATCH /guilds/{guild_id} updates a guild's settings. The happy path is +// gated on DISCORD_TEST_GUILD_ID and is non-destructive — we snapshot the +// guild's current name via getGuild and PATCH the same name back. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifier that should not match a real guild. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; + +describe("updateGuild", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - patches the guild with its existing name (non-destructive)", + async () => { + void testRunId; + const snapshot = await runEffect( + getGuild({ guild_id: TEST_GUILD_ID! }), + ); + const result = await runEffect( + updateGuild({ + guild_id: TEST_GUILD_ID!, + name: snapshot.name, + }), + ); + expect(result.id).toBe(TEST_GUILD_ID); + expect(typeof result.name).toBe("string"); + expect(result.name).toBe(snapshot.name); + expect(Array.isArray(result.roles)).toBe(true); + }, + { timeout: 30_000 }, + ); + + it("error - BadRequest for an empty name", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the BadRequest test", + ); + } + // Discord rejects an empty name with 400 BadRequest (name must be + // 2-100 chars). Routing may surface this as Forbidden or NotFound on + // edge cases. + await runEffect( + updateGuild({ + guild_id: TEST_GUILD_ID, + name: "", + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot cannot manage", async () => { + // A snowflake-shaped guild_id the bot cannot see typically yields 403 + // Forbidden (50001 Missing Access), or 404 NotFound if the route + // 404s before the permission check. + await runEffect( + updateGuild({ + guild_id: NON_EXISTENT_GUILD_ID, + name: `distilled-discord-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for a non-existent guild id", async () => { + const fakeGuildId = `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + updateGuild({ + guild_id: fakeGuildId, + name: `distilled-discord-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateGuildApplicationCommand.test.ts b/packages/discord/test/updateGuildApplicationCommand.test.ts new file mode 100644 index 000000000..6bdfcfe41 --- /dev/null +++ b/packages/discord/test/updateGuildApplicationCommand.test.ts @@ -0,0 +1,176 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { createGuildApplicationCommand } from "../src/operations/createGuildApplicationCommand.ts"; +import { deleteGuildApplicationCommand } from "../src/operations/deleteGuildApplicationCommand.ts"; +import { getMyApplication } from "../src/operations/getMyApplication.ts"; +import { updateGuildApplicationCommand } from "../src/operations/updateGuildApplicationCommand.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PATCH /applications/{application_id}/guilds/{guild_id}/commands/{command_id} +// updates a guild-scoped application command. The bot can only update its +// own commands. The happy path creates a fresh guild command, updates it, +// and deletes it in cleanup. Slash command names must be lowercase 1-32 +// chars matching ^[-_\p{L}\p{N}]+$. Gated on DISCORD_TEST_GUILD_ID. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +const commandName = (suffix: string) => + `dist-gupd-${suffix}-${testRunId}`.toLowerCase(); + +describe("updateGuildApplicationCommand", () => { + it.skipIf(!TEST_GUILD_ID)( + "happy path - updates the description of a guild application command", + async () => { + const me = await runEffect(getMyApplication({})); + const created = await runEffect( + createGuildApplicationCommand({ + application_id: me.id, + guild_id: TEST_GUILD_ID!, + name: commandName("orig"), + description: `original description ${testRunId}`, + }), + ); + try { + const newDescription = `updated description ${testRunId}`; + const result = await runEffect( + updateGuildApplicationCommand({ + application_id: me.id, + guild_id: TEST_GUILD_ID!, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + command_id: (created as any).id, + description: newDescription, + }), + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(result.id).toBe((created as any).id); + expect(result.application_id).toBe(me.id); + expect(typeof result.name).toBe("string"); + expect(result.description).toBe(newDescription); + expect(typeof result.version).toBe("string"); + } finally { + await runEffect( + deleteGuildApplicationCommand({ + application_id: me.id, + guild_id: TEST_GUILD_ID!, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + command_id: (created as any).id, + }).pipe(Effect.ignore), + ); + } + }, + { timeout: 60_000 }, + ); + + it.skipIf(!TEST_GUILD_ID)( + "error - BadRequest for an invalid name format", + async () => { + // Slash command names must be lowercase and match ^[-_\p{L}\p{N}]+$. + // An uppercase name with spaces should fail validation as + // BadRequest. Discord may also route as Forbidden or NotFound + // depending on access ordering. + const me = await runEffect(getMyApplication({})); + const created = await runEffect( + createGuildApplicationCommand({ + application_id: me.id, + guild_id: TEST_GUILD_ID!, + name: commandName("badname"), + description: `bad name test ${testRunId}`, + }), + ); + try { + await runEffect( + updateGuildApplicationCommand({ + application_id: me.id, + guild_id: TEST_GUILD_ID!, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + command_id: (created as any).id, + name: `Invalid Name With Spaces ${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + } finally { + await runEffect( + deleteGuildApplicationCommand({ + application_id: me.id, + guild_id: TEST_GUILD_ID!, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + command_id: (created as any).id, + }).pipe(Effect.ignore), + ); + } + }, + { timeout: 60_000 }, + ); + + it("error - Forbidden for an application the caller does not own", async () => { + // A real-looking snowflake the bot does not own. Discord may surface + // this as Forbidden, NotFound (to avoid leaking existence), or + // BadRequest. + const inaccessibleApplicationId = "100000000000000001"; + const inaccessibleGuildId = "100000000000000002"; + const fakeCommandId = "100000000000000003"; + await runEffect( + updateGuildApplicationCommand({ + application_id: inaccessibleApplicationId, + guild_id: inaccessibleGuildId, + command_id: fakeCommandId, + description: `forbidden ${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it.skipIf(!TEST_GUILD_ID)( + "error - NotFound for a non-existent command id", + async () => { + const me = await runEffect(getMyApplication({})); + const fakeCommandId = `1000000000000000${testRunId.slice(0, 2)}`; + await runEffect( + updateGuildApplicationCommand({ + application_id: me.id, + guild_id: TEST_GUILD_ID!, + command_id: fakeCommandId, + description: `nonexistent ${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may return NotFound (command does not exist), + // Forbidden (caller cannot see it), or BadRequest depending on + // routing. + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }, + ); +}); diff --git a/packages/discord/test/updateGuildEmoji.test.ts b/packages/discord/test/updateGuildEmoji.test.ts new file mode 100644 index 000000000..2026e845d --- /dev/null +++ b/packages/discord/test/updateGuildEmoji.test.ts @@ -0,0 +1,173 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildEmoji } from "../src/operations/createGuildEmoji.ts"; +import { deleteGuildEmoji } from "../src/operations/deleteGuildEmoji.ts"; +import { updateGuildEmoji } from "../src/operations/updateGuildEmoji.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Smallest valid 1x1 transparent PNG, encoded as a data URI. Discord accepts +// data URIs of the form "data:image/{png,jpeg,gif};base64,...". +const TINY_PNG_DATA_URI = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII="; + +// Requires a guild where the bot has Manage Emojis and Stickers. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifiers that should not match real entities. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_EMOJI_ID = "100000000000000001"; + +// Discord requires emoji names to match ^[a-zA-Z0-9_]{2,32}$. +const emojiName = (suffix: string): string => { + const raw = `dt_${suffix}_${testRunId}`; + return raw.replace(/[^a-zA-Z0-9_]/g, "_").slice(0, 32); +}; + +describe("updateGuildEmoji", () => { + it( + "happy path - renames a freshly created guild emoji", + async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the updateGuildEmoji happy path", + ); + } + const originalName = emojiName("upd_o"); + const newName = emojiName("upd_n"); + await runEffect( + Effect.gen(function* () { + const emoji = yield* createGuildEmoji({ + guild_id: TEST_GUILD_ID, + name: originalName, + image: TINY_PNG_DATA_URI, + }); + return yield* Effect.gen(function* () { + const updated = yield* updateGuildEmoji({ + guild_id: TEST_GUILD_ID, + emoji_id: emoji.id, + name: newName, + }); + return yield* Effect.sync(() => { + expect(updated.id).toBe(emoji.id); + expect(updated.name).toBe(newName); + expect(Array.isArray(updated.roles)).toBe(true); + expect(typeof updated.require_colons).toBe("boolean"); + expect(typeof updated.managed).toBe("boolean"); + expect(typeof updated.animated).toBe("boolean"); + expect(typeof updated.available).toBe("boolean"); + }); + }).pipe( + Effect.ensuring( + deleteGuildEmoji({ + guild_id: TEST_GUILD_ID, + emoji_id: emoji.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for non-existent emoji_id on a real guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + // A snowflake-shaped emoji_id that does not exist on the guild yields + // 404 NotFound. Discord may also surface 403 Forbidden depending on + // which check fires first. + await runEffect( + updateGuildEmoji({ + guild_id: TEST_GUILD_ID, + emoji_id: NON_EXISTENT_EMOJI_ID, + name: emojiName("nf"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden"]).toContain((e as any)._tag); + }), + ), + ); + }); + + it("error - BadRequest for invalid emoji name (contains hyphens and spaces)", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the BadRequest test", + ); + } + // Discord's emoji names must match ^[a-zA-Z0-9_]{2,32}$ — hyphens and + // spaces are rejected with 400 Invalid Form Body. We need a real emoji + // for the route to actually validate the body, so create-then-update. + const original = emojiName("br_o"); + await runEffect( + Effect.gen(function* () { + const emoji = yield* createGuildEmoji({ + guild_id: TEST_GUILD_ID, + name: original, + image: TINY_PNG_DATA_URI, + }); + return yield* updateGuildEmoji({ + guild_id: TEST_GUILD_ID, + emoji_id: emoji.id, + name: "bad-name with spaces", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + Effect.ensuring( + deleteGuildEmoji({ + guild_id: TEST_GUILD_ID, + emoji_id: emoji.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, 30_000); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // A guild_id the bot does not see typically yields 403 Forbidden + // (50001 Missing Access), or 404 NotFound if the route 404s before the + // permission check. + await runEffect( + updateGuildEmoji({ + guild_id: NON_EXISTENT_GUILD_ID, + emoji_id: NON_EXISTENT_EMOJI_ID, + name: emojiName("fb"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateGuildMember.test.ts b/packages/discord/test/updateGuildMember.test.ts new file mode 100644 index 000000000..17c20163b --- /dev/null +++ b/packages/discord/test/updateGuildMember.test.ts @@ -0,0 +1,163 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { updateGuildMember } from "../src/operations/updateGuildMember.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint requires: +// - a guild the bot is in with MANAGE_NICKNAMES (and other related perms +// depending on which fields are touched). +// - the user_id (snowflake) of a member of that guild. The bot is itself +// a member, so DISCORD_TEST_BOT_USER_ID is a reliable target. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_BOT_USER_ID = process.env.DISCORD_TEST_BOT_USER_ID; + +// Snowflake-format identifiers that should not match real resources. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_USER_ID = "100000000000000001"; + +// Discord nicknames must be 1–32 characters; build short, run-scoped values. +const nickFor = (suffix: string): string => + `dt_${suffix}_${testRunId}`.slice(0, 32); + +describe("updateGuildMember", () => { + it( + "happy path - updates the bot's own nickname in the test guild", + async () => { + if (!TEST_GUILD_ID || !TEST_BOT_USER_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_BOT_USER_ID env vars are required for the updateGuildMember happy path", + ); + } + const newNick = nickFor("happy"); + await runEffect( + Effect.gen(function* () { + const updated = yield* updateGuildMember({ + guild_id: TEST_GUILD_ID, + user_id: TEST_BOT_USER_ID, + nick: newNick, + }); + return yield* Effect.sync(() => { + expect(updated.user.id).toBe(TEST_BOT_USER_ID); + expect(updated.nick).toBe(newNick); + expect(Array.isArray(updated.roles)).toBe(true); + expect(typeof updated.joined_at).toBe("string"); + expect(typeof updated.flags).toBe("number"); + expect(typeof updated.pending).toBe("boolean"); + expect(typeof updated.mute).toBe("boolean"); + expect(typeof updated.deaf).toBe("boolean"); + }).pipe( + // Restore the nickname to null so the test guild is not left in + // a dirtied state between runs. + Effect.ensuring( + updateGuildMember({ + guild_id: TEST_GUILD_ID, + user_id: TEST_BOT_USER_ID, + nick: null, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for a user that is not a member of the guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + // A snowflake-shaped user_id with no membership in the guild yields + // 404 NotFound. Discord may also surface 403 Forbidden depending on + // which check fires first. + await runEffect( + updateGuildMember({ + guild_id: TEST_GUILD_ID, + user_id: NON_EXISTENT_USER_ID, + nick: nickFor("nf"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - BadRequest for nickname exceeding 32 characters", async () => { + if (!TEST_GUILD_ID || !TEST_BOT_USER_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_BOT_USER_ID env vars are required for the BadRequest test", + ); + } + // Discord nicknames must be 1–32 characters; a 64-character value is + // rejected with 400 Invalid Form Body. + const tooLongNick = "a".repeat(64); + await runEffect( + updateGuildMember({ + guild_id: TEST_GUILD_ID, + user_id: TEST_BOT_USER_ID, + nick: tooLongNick, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + // If the API somehow accepted the nick, restore it to null so the + // bot's display name in the test guild is not left dirty. + Effect.ensuring( + updateGuildMember({ + guild_id: TEST_GUILD_ID, + user_id: TEST_BOT_USER_ID, + nick: null, + }).pipe(Effect.ignore), + ), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // A guild_id the bot does not see typically yields 403 Forbidden + // (50001 Missing Access), or 404 NotFound if the route 404s before the + // permission check. + await runEffect( + updateGuildMember({ + guild_id: NON_EXISTENT_GUILD_ID, + user_id: NON_EXISTENT_USER_ID, + nick: nickFor("fb"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateGuildRole.test.ts b/packages/discord/test/updateGuildRole.test.ts new file mode 100644 index 000000000..42b729cc7 --- /dev/null +++ b/packages/discord/test/updateGuildRole.test.ts @@ -0,0 +1,179 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildRole } from "../src/operations/createGuildRole.ts"; +import { deleteGuildRole } from "../src/operations/deleteGuildRole.ts"; +import { updateGuildRole } from "../src/operations/updateGuildRole.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint requires: +// - a guild the bot is in with MANAGE_ROLES permission. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifiers that should not match real resources. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_ROLE_ID = "100000000000000001"; + +// Discord role names: 1..100 chars; we keep them short. +const roleName = (suffix: string): string => + `dtest-${suffix}-${testRunId}`.slice(0, 100); + +describe("updateGuildRole", () => { + it( + "happy path - renames a freshly created guild role", + async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the updateGuildRole happy path", + ); + } + const originalName = roleName("upd_o"); + const newName = roleName("upd_n"); + await runEffect( + Effect.gen(function* () { + const role = yield* createGuildRole({ + guild_id: TEST_GUILD_ID, + name: originalName, + mentionable: false, + hoist: false, + }); + return yield* Effect.gen(function* () { + const updated = yield* updateGuildRole({ + guild_id: TEST_GUILD_ID, + role_id: role.id, + name: newName, + mentionable: true, + }); + return yield* Effect.sync(() => { + expect(updated.id).toBe(role.id); + expect(updated.name).toBe(newName); + expect(updated.mentionable).toBe(true); + expect(typeof updated.permissions).toBe("string"); + expect(typeof updated.position).toBe("number"); + expect(typeof updated.color).toBe("number"); + expect(typeof updated.flags).toBe("number"); + expect(typeof updated.hoist).toBe("boolean"); + expect(typeof updated.managed).toBe("boolean"); + }); + }).pipe( + Effect.ensuring( + deleteGuildRole({ + guild_id: TEST_GUILD_ID, + role_id: role.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for non-existent role_id on a real guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + // A snowflake-shaped role_id that does not exist on the guild yields + // 404 NotFound. Discord may also surface 403 Forbidden depending on + // which check fires first. + await runEffect( + updateGuildRole({ + guild_id: TEST_GUILD_ID, + role_id: NON_EXISTENT_ROLE_ID, + name: roleName("nf"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it( + "error - BadRequest for permissions value out of bitfield range", + async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the BadRequest test", + ); + } + // Discord's permissions field is a 64-bit integer bitfield. Negative + // values are rejected with 400 Invalid Form Body. We need a real role + // for the route to actually validate the body, so create-then-update. + const original = roleName("br_o"); + await runEffect( + Effect.gen(function* () { + const role = yield* createGuildRole({ + guild_id: TEST_GUILD_ID, + name: original, + mentionable: false, + hoist: false, + }); + return yield* updateGuildRole({ + guild_id: TEST_GUILD_ID, + role_id: role.id, + permissions: -1, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + Effect.ensuring( + deleteGuildRole({ + guild_id: TEST_GUILD_ID, + role_id: role.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // A guild_id the bot does not see typically yields 403 Forbidden + // (50001 Missing Access), or 404 NotFound if the route 404s before the + // permission check. + await runEffect( + updateGuildRole({ + guild_id: NON_EXISTENT_GUILD_ID, + role_id: NON_EXISTENT_ROLE_ID, + name: roleName("fb"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateGuildScheduledEvent.test.ts b/packages/discord/test/updateGuildScheduledEvent.test.ts new file mode 100644 index 000000000..2d0812cea --- /dev/null +++ b/packages/discord/test/updateGuildScheduledEvent.test.ts @@ -0,0 +1,161 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildScheduledEvent } from "../src/operations/createGuildScheduledEvent.ts"; +import { deleteGuildScheduledEvent } from "../src/operations/deleteGuildScheduledEvent.ts"; +import { updateGuildScheduledEvent } from "../src/operations/updateGuildScheduledEvent.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); +// Both update and create scheduled-event input schemas currently expose only +// path params (no body fields), so there are no run-scoped resource names to +// inject testRunId into. It is still declared so the helper exists if the +// schema is patched to accept name/start-time fields later. +void testRunId; + +// The endpoint requires: +// - a guild the bot is in with MANAGE_EVENTS permission. +// The SDK's input schema for both create and update currently only exposes +// path params (not the body). createGuildScheduledEvent will send an empty +// body which Discord typically rejects with 400; the happy path here is +// best-effort against a real guild. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifiers that should not match real resources. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_EVENT_ID = "100000000000000001"; + +describe("updateGuildScheduledEvent", () => { + it( + "happy path - patches a freshly created scheduled event", + async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the updateGuildScheduledEvent happy path", + ); + } + await runEffect( + Effect.gen(function* () { + // The create operation's output is opaque (Schema.Unknown) — cast + // to extract the id we need to drive update + cleanup. + const eventRaw = yield* createGuildScheduledEvent({ + guild_id: TEST_GUILD_ID, + }); + const event = eventRaw as { id?: string }; + if (!event.id) { + throw new Error( + "createGuildScheduledEvent did not return an id — cannot exercise updateGuildScheduledEvent happy path", + ); + } + return yield* Effect.gen(function* () { + const updated = yield* updateGuildScheduledEvent({ + guild_id: TEST_GUILD_ID, + guild_scheduled_event_id: event.id!, + }); + return yield* Effect.sync(() => { + // Output schema is Schema.Unknown — assert it round-tripped to + // an object shape and surfaces the same id. + expect(typeof updated).toBe("object"); + const u = updated as { id?: string }; + if (u.id !== undefined) { + expect(u.id).toBe(event.id); + } + }); + }).pipe( + Effect.ensuring( + deleteGuildScheduledEvent({ + guild_id: TEST_GUILD_ID, + guild_scheduled_event_id: event.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for non-existent guild_scheduled_event_id on a real guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + // A snowflake-shaped event id that does not exist on the guild yields + // 404 NotFound. Discord may also surface 403 Forbidden depending on + // which check fires first. + await runEffect( + updateGuildScheduledEvent({ + guild_id: TEST_GUILD_ID, + guild_scheduled_event_id: NON_EXISTENT_EVENT_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) guild_scheduled_event_id", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the BadRequest test", + ); + } + // Discord rejects malformed snowflakes with 400 Invalid Form Body; + // routing layers may also classify the path as 404, or the bot may lack + // permission and receive 403. + await runEffect( + updateGuildScheduledEvent({ + guild_id: TEST_GUILD_ID, + guild_scheduled_event_id: "not-a-snowflake", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // A guild_id the bot does not see typically yields 403 Forbidden + // (50001 Missing Access), or 404 NotFound if the route 404s before the + // permission check. + await runEffect( + updateGuildScheduledEvent({ + guild_id: NON_EXISTENT_GUILD_ID, + guild_scheduled_event_id: NON_EXISTENT_EVENT_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateGuildSoundboardSound.test.ts b/packages/discord/test/updateGuildSoundboardSound.test.ts new file mode 100644 index 000000000..77659ff5e --- /dev/null +++ b/packages/discord/test/updateGuildSoundboardSound.test.ts @@ -0,0 +1,184 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildSoundboardSound } from "../src/operations/createGuildSoundboardSound.ts"; +import { deleteGuildSoundboardSound } from "../src/operations/deleteGuildSoundboardSound.ts"; +import { updateGuildSoundboardSound } from "../src/operations/updateGuildSoundboardSound.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint requires: +// - a guild the bot is in with CREATE_GUILD_EXPRESSIONS permission and +// soundboard support (community guild or boosted). +// - a sound data URI: "data:audio/{mpeg,ogg};base64,..." up to 512KB and +// <= 5.2 seconds duration. Operators must supply their own valid clip +// via DISCORD_TEST_SOUNDBOARD_DATA_URI; no inline MP3/OGG fixture is +// small enough to embed safely. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_SOUND_DATA_URI = process.env.DISCORD_TEST_SOUNDBOARD_DATA_URI; + +// Snowflake-format identifiers that should not match real resources. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_SOUND_ID = "100000000000000001"; + +// Discord requires soundboard sound names of 2..32 chars. +const soundName = (suffix: string): string => + `dt-${suffix}-${testRunId}`.slice(0, 32); + +describe("updateGuildSoundboardSound", () => { + it( + "happy path - renames a freshly created soundboard sound", + async () => { + if (!TEST_GUILD_ID || !TEST_SOUND_DATA_URI) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_SOUNDBOARD_DATA_URI env vars are required for the updateGuildSoundboardSound happy path", + ); + } + const originalName = soundName("upd_o"); + const newName = soundName("upd_n"); + await runEffect( + Effect.gen(function* () { + const sound = yield* createGuildSoundboardSound({ + guild_id: TEST_GUILD_ID, + name: originalName, + sound: TEST_SOUND_DATA_URI, + volume: 1, + }); + return yield* Effect.gen(function* () { + const updated = yield* updateGuildSoundboardSound({ + guild_id: TEST_GUILD_ID, + sound_id: sound.sound_id, + name: newName, + volume: 0.5, + }); + return yield* Effect.sync(() => { + expect(updated.sound_id).toBe(sound.sound_id); + expect(updated.name).toBe(newName); + expect(typeof updated.volume).toBe("number"); + expect(typeof updated.available).toBe("boolean"); + if (updated.guild_id !== undefined) { + expect(updated.guild_id).toBe(TEST_GUILD_ID); + } + }); + }).pipe( + Effect.ensuring( + deleteGuildSoundboardSound({ + guild_id: TEST_GUILD_ID, + sound_id: sound.sound_id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for non-existent sound_id on a real guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + // A snowflake-shaped sound_id that does not exist on the guild yields + // 404 NotFound. Discord may also surface 403 Forbidden depending on + // which check fires first. + await runEffect( + updateGuildSoundboardSound({ + guild_id: TEST_GUILD_ID, + sound_id: NON_EXISTENT_SOUND_ID, + name: soundName("nf"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it( + "error - BadRequest for name shorter than 2 characters", + async () => { + if (!TEST_GUILD_ID || !TEST_SOUND_DATA_URI) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_SOUNDBOARD_DATA_URI env vars are required for the BadRequest test", + ); + } + // Discord requires soundboard sound names to be 2–32 characters; a + // single-character name is rejected with 400 Invalid Form Body. May + // also surface as Forbidden if MANAGE_GUILD_EXPRESSIONS validation + // fires first. + const original = soundName("br_o"); + await runEffect( + Effect.gen(function* () { + const sound = yield* createGuildSoundboardSound({ + guild_id: TEST_GUILD_ID, + name: original, + sound: TEST_SOUND_DATA_URI, + volume: 1, + }); + return yield* updateGuildSoundboardSound({ + guild_id: TEST_GUILD_ID, + sound_id: sound.sound_id, + name: "x", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + Effect.ensuring( + deleteGuildSoundboardSound({ + guild_id: TEST_GUILD_ID, + sound_id: sound.sound_id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // A guild_id the bot does not see typically yields 403 Forbidden + // (50001 Missing Access), or 404 NotFound if the route 404s before the + // permission check. + await runEffect( + updateGuildSoundboardSound({ + guild_id: NON_EXISTENT_GUILD_ID, + sound_id: NON_EXISTENT_SOUND_ID, + name: soundName("fb"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateGuildSticker.test.ts b/packages/discord/test/updateGuildSticker.test.ts new file mode 100644 index 000000000..553ac650f --- /dev/null +++ b/packages/discord/test/updateGuildSticker.test.ts @@ -0,0 +1,182 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildSticker } from "../src/operations/createGuildSticker.ts"; +import { deleteGuildSticker } from "../src/operations/deleteGuildSticker.ts"; +import { updateGuildSticker } from "../src/operations/updateGuildSticker.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// The endpoint requires: +// - a guild the bot is in with MANAGE_GUILD_EXPRESSIONS permission. +// - to drive the happy path we first create a real sticker, which needs a +// PNG/APNG/Lottie data URI at exactly 320x320 and <= 512KB. Operators +// must supply their own clip via DISCORD_TEST_STICKER_DATA_URI; no +// inline fixture meets the size + dimension constraints safely. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; +const TEST_STICKER_DATA_URI = process.env.DISCORD_TEST_STICKER_DATA_URI; + +// Snowflake-format identifiers that should not match real resources. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +const NON_EXISTENT_STICKER_ID = "100000000000000001"; + +// Discord requires sticker names of 2..30 chars. +const stickerName = (suffix: string): string => + `dt-${suffix}-${testRunId}`.slice(0, 30); + +describe("updateGuildSticker", () => { + it( + "happy path - renames a freshly created guild sticker", + async () => { + if (!TEST_GUILD_ID || !TEST_STICKER_DATA_URI) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_STICKER_DATA_URI env vars are required for the updateGuildSticker happy path", + ); + } + const originalName = stickerName("upd_o"); + const newName = stickerName("upd_n"); + await runEffect( + Effect.gen(function* () { + const sticker = yield* createGuildSticker({ + guild_id: TEST_GUILD_ID, + name: originalName, + tags: "smile", + description: "distilled test sticker", + file: TEST_STICKER_DATA_URI, + }); + return yield* Effect.gen(function* () { + const updated = yield* updateGuildSticker({ + guild_id: TEST_GUILD_ID, + sticker_id: sticker.id, + name: newName, + description: "renamed by distilled", + }); + return yield* Effect.sync(() => { + expect(updated.id).toBe(sticker.id); + expect(updated.name).toBe(newName); + expect(updated.description).toBe("renamed by distilled"); + expect(typeof updated.tags).toBe("string"); + expect(typeof updated.available).toBe("boolean"); + expect(updated.guild_id).toBe(TEST_GUILD_ID); + }); + }).pipe( + Effect.ensuring( + deleteGuildSticker({ + guild_id: TEST_GUILD_ID, + sticker_id: sticker.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for non-existent sticker_id on a real guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + // A snowflake-shaped sticker_id that does not exist on the guild yields + // 404 NotFound. Discord may also surface 403 Forbidden depending on + // which check fires first. + await runEffect( + updateGuildSticker({ + guild_id: TEST_GUILD_ID, + sticker_id: NON_EXISTENT_STICKER_ID, + name: stickerName("nf"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it( + "error - BadRequest for name shorter than 2 characters", + async () => { + if (!TEST_GUILD_ID || !TEST_STICKER_DATA_URI) { + throw new Error( + "DISCORD_TEST_GUILD_ID and DISCORD_TEST_STICKER_DATA_URI env vars are required for the BadRequest test", + ); + } + // Discord requires sticker names to be 2–30 characters; a single-char + // name is rejected with 400 Invalid Form Body. We need a real sticker + // for the route to actually validate the body, so create-then-update. + const original = stickerName("br_o"); + await runEffect( + Effect.gen(function* () { + const sticker = yield* createGuildSticker({ + guild_id: TEST_GUILD_ID, + name: original, + tags: "smile", + file: TEST_STICKER_DATA_URI, + }); + return yield* updateGuildSticker({ + guild_id: TEST_GUILD_ID, + sticker_id: sticker.id, + name: "x", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + Effect.ensuring( + deleteGuildSticker({ + guild_id: TEST_GUILD_ID, + sticker_id: sticker.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // A guild_id the bot does not see typically yields 403 Forbidden + // (50001 Missing Access), or 404 NotFound if the route 404s before the + // permission check. + await runEffect( + updateGuildSticker({ + guild_id: NON_EXISTENT_GUILD_ID, + sticker_id: NON_EXISTENT_STICKER_ID, + name: stickerName("fb"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateGuildTemplate.test.ts b/packages/discord/test/updateGuildTemplate.test.ts new file mode 100644 index 000000000..9e50ef27a --- /dev/null +++ b/packages/discord/test/updateGuildTemplate.test.ts @@ -0,0 +1,177 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createGuildTemplate } from "../src/operations/createGuildTemplate.ts"; +import { deleteGuildTemplate } from "../src/operations/deleteGuildTemplate.ts"; +import { updateGuildTemplate } from "../src/operations/updateGuildTemplate.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a guild the bot is in with MANAGE_GUILD permission. Each guild can +// only have one template at a time, so each test creates and immediately +// deletes its own template. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifier that should not match a real guild. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; +// A template code that should not match any real template on a real guild. +const NON_EXISTENT_TEMPLATE_CODE = `nope-${testRunId}`; + +// Discord requires template names of 1..100 chars. +const templateName = (suffix: string): string => + `dt-${suffix}-${testRunId}`.slice(0, 100); + +describe("updateGuildTemplate", () => { + it( + "happy path - renames a freshly created guild template", + async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the updateGuildTemplate happy path", + ); + } + const originalName = templateName("upd_o"); + const newName = templateName("upd_n"); + await runEffect( + Effect.gen(function* () { + const template = yield* createGuildTemplate({ + guild_id: TEST_GUILD_ID, + name: originalName, + description: "distilled test template", + }); + return yield* Effect.gen(function* () { + const updated = yield* updateGuildTemplate({ + guild_id: TEST_GUILD_ID, + code: template.code, + name: newName, + description: "renamed by distilled", + }); + return yield* Effect.sync(() => { + expect(updated.code).toBe(template.code); + expect(updated.name).toBe(newName); + expect(updated.description).toBe("renamed by distilled"); + expect(updated.source_guild_id).toBe(TEST_GUILD_ID); + expect(typeof updated.usage_count).toBe("number"); + expect(typeof updated.creator_id).toBe("string"); + expect(typeof updated.created_at).toBe("string"); + expect(typeof updated.updated_at).toBe("string"); + }); + }).pipe( + Effect.ensuring( + deleteGuildTemplate({ + guild_id: TEST_GUILD_ID, + code: template.code, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for non-existent template code on a real guild", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the NotFound test", + ); + } + // A template code that does not exist on the guild yields 404 NotFound. + // Discord may also surface 403 Forbidden depending on which check fires + // first. + await runEffect( + updateGuildTemplate({ + guild_id: TEST_GUILD_ID, + code: NON_EXISTENT_TEMPLATE_CODE, + name: templateName("nf"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it( + "error - BadRequest when name is empty", + async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the BadRequest test", + ); + } + // Name must be 1..100 characters; empty string is rejected with 400 + // Invalid Form Body. We need a real template for the route to actually + // validate the body, so create-then-update. + const original = templateName("br_o"); + await runEffect( + Effect.gen(function* () { + const template = yield* createGuildTemplate({ + guild_id: TEST_GUILD_ID, + name: original, + }); + return yield* updateGuildTemplate({ + guild_id: TEST_GUILD_ID, + code: template.code, + name: "", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + Effect.ensuring( + deleteGuildTemplate({ + guild_id: TEST_GUILD_ID, + code: template.code, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // A guild_id the bot does not see typically yields 403 Forbidden + // (50001 Missing Access), or 404 NotFound if the route 404s before the + // permission check. + await runEffect( + updateGuildTemplate({ + guild_id: NON_EXISTENT_GUILD_ID, + code: NON_EXISTENT_TEMPLATE_CODE, + name: templateName("fb"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateGuildWelcomeScreen.test.ts b/packages/discord/test/updateGuildWelcomeScreen.test.ts new file mode 100644 index 000000000..cb901150c --- /dev/null +++ b/packages/discord/test/updateGuildWelcomeScreen.test.ts @@ -0,0 +1,161 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getGuildWelcomeScreen } from "../src/operations/getGuildWelcomeScreen.ts"; +import { updateGuildWelcomeScreen } from "../src/operations/updateGuildWelcomeScreen.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PATCH /guilds/{guild_id}/welcome-screen requires: +// - a Community-enabled guild that has a welcome screen configured +// (Discord returns 404 10069 otherwise). +// - MANAGE_GUILD permission. +const TEST_GUILD_ID = + process.env.DISCORD_TEST_GUILD_WITH_WELCOME_SCREEN_ID ?? + process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids that should not resolve to any guild the bot can +// read. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +// Welcome screen description is limited to 140 characters; build run-scoped +// values short enough to fit comfortably. +const description = (suffix: string): string => + `dt-${suffix}-${testRunId}`.slice(0, 140); + +describe("updateGuildWelcomeScreen", () => { + it( + "happy path - updates the welcome screen description and restores it", + async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_WITH_WELCOME_SCREEN_ID (or DISCORD_TEST_GUILD_ID) must be set " + + "for the updateGuildWelcomeScreen happy path. The guild must be Community-enabled " + + "with a welcome screen configured.", + ); + } + const newDescription = description("happy"); + await runEffect( + Effect.gen(function* () { + // Snapshot the current description so we can restore it post-test + // and not pollute the test guild. + const original = yield* getGuildWelcomeScreen({ + guild_id: TEST_GUILD_ID, + }); + return yield* Effect.gen(function* () { + const updated = yield* updateGuildWelcomeScreen({ + guild_id: TEST_GUILD_ID, + description: newDescription, + }); + return yield* Effect.sync(() => { + expect(updated.description).toBe(newDescription); + expect(Array.isArray(updated.welcome_channels)).toBe(true); + for (const wc of updated.welcome_channels) { + expect(typeof wc.channel_id).toBe("string"); + expect(typeof wc.description).toBe("string"); + expect( + wc.emoji_name === null || typeof wc.emoji_name === "string", + ).toBe(true); + } + }); + }).pipe( + Effect.ensuring( + updateGuildWelcomeScreen({ + guild_id: TEST_GUILD_ID, + description: original.description, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for a non-existent guild id", async () => { + await runEffect( + updateGuildWelcomeScreen({ + guild_id: NON_EXISTENT_GUILD_ID, + description: description("nf"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may surface a missing guild as NotFound (10004), or as + // Forbidden (Missing Access) when the bot is not in the guild. + // Some malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it( + "error - BadRequest for description exceeding the 140 character limit", + async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_WITH_WELCOME_SCREEN_ID (or DISCORD_TEST_GUILD_ID) is required for the BadRequest test", + ); + } + // Welcome screen descriptions are limited to 140 chars; a 200-char + // string is rejected with 400 Invalid Form Body. May also surface as + // Forbidden if MANAGE_GUILD validation fires first, or NotFound on a + // non-Community guild. + const tooLong = "a".repeat(200); + await runEffect( + updateGuildWelcomeScreen({ + guild_id: TEST_GUILD_ID, + description: tooLong, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }, + 30_000, + ); + + it("error - Forbidden for a guild the bot cannot access", async () => { + // A guild_id the bot does not see typically yields 403 Forbidden + // (50001 Missing Access), or 404 NotFound if the route 404s before the + // permission check. + await runEffect( + updateGuildWelcomeScreen({ + guild_id: INACCESSIBLE_GUILD_ID, + description: description("fb"), + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateGuildWidgetSettings.test.ts b/packages/discord/test/updateGuildWidgetSettings.test.ts new file mode 100644 index 000000000..7bd8636bc --- /dev/null +++ b/packages/discord/test/updateGuildWidgetSettings.test.ts @@ -0,0 +1,141 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getGuildWidgetSettings } from "../src/operations/getGuildWidgetSettings.ts"; +import { updateGuildWidgetSettings } from "../src/operations/updateGuildWidgetSettings.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PATCH /guilds/{guild_id}/widget updates widget settings (enabled + target +// channel). Requires MANAGE_GUILD on the bot's member of the guild. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids unlikely to resolve to a guild the bot can access. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_GUILD_ID = "100000000000000001"; + +describe("updateGuildWidgetSettings", () => { + it( + "happy path - toggles the widget enabled flag and restores it", + async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID must be set for the updateGuildWidgetSettings happy path. " + + "The bot must have MANAGE_GUILD on this guild.", + ); + } + await runEffect( + Effect.gen(function* () { + // Snapshot current settings so we can restore afterwards and not + // leave the test guild in a flipped state. + const original = yield* getGuildWidgetSettings({ + guild_id: TEST_GUILD_ID, + }); + const flipped = !original.enabled; + return yield* Effect.gen(function* () { + const updated = yield* updateGuildWidgetSettings({ + guild_id: TEST_GUILD_ID, + enabled: flipped, + }); + return yield* Effect.sync(() => { + expect(updated.enabled).toBe(flipped); + // channel_id is opaque on the response — Discord returns + // either a snowflake string or null. We assert only that the + // property round-tripped on the response. + expect("channel_id" in updated).toBe(true); + }); + }).pipe( + Effect.ensuring( + updateGuildWidgetSettings({ + guild_id: TEST_GUILD_ID, + enabled: original.enabled, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for a non-existent guild id", async () => { + await runEffect( + updateGuildWidgetSettings({ + guild_id: NON_EXISTENT_GUILD_ID, + enabled: true, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord may surface a missing guild as NotFound (10004), or as + // Forbidden (Missing Access) when the bot is not in the guild. + // Some malformed snowflakes may surface as BadRequest. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) channel_id", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID is required for the BadRequest test", + ); + } + // channel_id must be a snowflake string (or null) referring to a channel + // in the same guild. A non-snowflake value is rejected with 400 Invalid + // Form Body. May also surface as Forbidden if MANAGE_GUILD validation + // fires first. + await runEffect( + updateGuildWidgetSettings({ + guild_id: TEST_GUILD_ID, + channel_id: "not-a-snowflake", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for a guild the bot cannot access", async () => { + // A guild_id the bot does not see typically yields 403 Forbidden + // (50001 Missing Access), or 404 NotFound if the route 404s before the + // permission check. + await runEffect( + updateGuildWidgetSettings({ + guild_id: INACCESSIBLE_GUILD_ID, + enabled: true, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateInviteTargetUsers.test.ts b/packages/discord/test/updateInviteTargetUsers.test.ts new file mode 100644 index 000000000..639b082e3 --- /dev/null +++ b/packages/discord/test/updateInviteTargetUsers.test.ts @@ -0,0 +1,133 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { updateInviteTargetUsers } from "../src/operations/updateInviteTargetUsers.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PUT /invites/{code}/target-users — replaces the target users associated +// with an invite (used for "join activity" / stream invites). The body is +// a multipart request with a `target_users_file` field carrying the new +// list of user ids. Output schema is Void; a successful call resolves +// with `undefined`. +// +// This endpoint is restrictive: most regular invites return 404. Operators +// must supply DISCORD_TEST_INVITE_TARGET_USERS_CODE pointing at an invite +// that already has target users, plus a payload via +// DISCORD_TEST_INVITE_TARGET_USERS_FILE. +const TEST_INVITE_CODE = process.env.DISCORD_TEST_INVITE_TARGET_USERS_CODE; +const TEST_TARGET_USERS_FILE = + process.env.DISCORD_TEST_INVITE_TARGET_USERS_FILE; + +// Invite codes are short opaque strings, not snowflakes. A made-up code +// that does not match any real invite should yield NotFound. +const NON_EXISTENT_INVITE_CODE = `distilled-no-such-${testRunId}`; +const REVOKED_OR_INACCESSIBLE_CODE = `distilled-fb-${testRunId}`; + +// A clearly invalid target_users_file payload — empty string — used for +// the BadRequest path; Discord rejects it with 400 Invalid Form Body. +const INVALID_TARGET_USERS_FILE = ""; + +describe("updateInviteTargetUsers", () => { + it( + "happy path - replaces target users on a real invite", + async () => { + if (!TEST_INVITE_CODE || !TEST_TARGET_USERS_FILE) { + throw new Error( + "DISCORD_TEST_INVITE_TARGET_USERS_CODE and DISCORD_TEST_INVITE_TARGET_USERS_FILE " + + "env vars are required for the updateInviteTargetUsers happy path. The invite " + + "must be one with target users (e.g. a stream / activity invite).", + ); + } + const result = await runEffect( + updateInviteTargetUsers({ + code: TEST_INVITE_CODE, + target_users_file: TEST_TARGET_USERS_FILE, + }), + ); + // Output schema is Void — a successful resolution is the assertion. + expect(result).toBeUndefined(); + }, + 30_000, + ); + + it("error - NotFound for a non-existent invite code", async () => { + await runEffect( + updateInviteTargetUsers({ + code: NON_EXISTENT_INVITE_CODE, + target_users_file: TEST_TARGET_USERS_FILE ?? "placeholder", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord typically returns NotFound (10006 — invalid invite) for + // a code that does not match any invite. Some malformed codes may + // surface as BadRequest, and revoked codes as Forbidden. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - BadRequest for an empty target_users_file payload", async () => { + if (!TEST_INVITE_CODE) { + throw new Error( + "DISCORD_TEST_INVITE_TARGET_USERS_CODE is required for the BadRequest test", + ); + } + // An empty target_users_file is rejected with 400 Invalid Form Body. + // May also surface as Forbidden if the bot lacks permission on the + // invite, or NotFound if the invite is no longer resolvable. + await runEffect( + updateInviteTargetUsers({ + code: TEST_INVITE_CODE, + target_users_file: INVALID_TARGET_USERS_FILE, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden or NotFound for an obviously invalid code shape", async () => { + // Calling with a code that cannot be resolved typically yields NotFound, + // but the route may reject it as Forbidden / BadRequest depending on + // which validation layer fires first. + await runEffect( + updateInviteTargetUsers({ + code: `${REVOKED_OR_INACCESSIBLE_CODE}-x!`, + target_users_file: TEST_TARGET_USERS_FILE ?? "placeholder", + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateLobbyMessageExternalModerationMetadata.test.ts b/packages/discord/test/updateLobbyMessageExternalModerationMetadata.test.ts new file mode 100644 index 000000000..b0e35e87c --- /dev/null +++ b/packages/discord/test/updateLobbyMessageExternalModerationMetadata.test.ts @@ -0,0 +1,119 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createLobby } from "../src/operations/createLobby.ts"; +import { createLobbyMessage } from "../src/operations/createLobbyMessage.ts"; +import { updateLobbyMessageExternalModerationMetadata } from "../src/operations/updateLobbyMessageExternalModerationMetadata.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Snowflake-format identifiers that should not match real resources. +const NON_EXISTENT_LOBBY_ID = "100000000000000000"; +const NON_EXISTENT_MESSAGE_ID = "100000000000000001"; + +describe("updateLobbyMessageExternalModerationMetadata", () => { + it( + "happy path - updates moderation metadata for a freshly posted lobby message", + async () => { + await runEffect( + Effect.gen(function* () { + const lobby = yield* createLobby({ idle_timeout_seconds: 5 }); + const msg = yield* createLobbyMessage({ + lobby_id: lobby.id, + content: `distilled-mod-${testRunId}`, + }); + const result = yield* updateLobbyMessageExternalModerationMetadata({ + lobby_id: lobby.id, + message_id: msg.id, + }); + return yield* Effect.sync(() => { + // Output schema is Void — successful resolution is the assertion. + expect(result).toBeUndefined(); + }); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for non-existent message_id on a real lobby", async () => { + await runEffect( + Effect.gen(function* () { + const lobby = yield* createLobby({ idle_timeout_seconds: 5 }); + return yield* updateLobbyMessageExternalModerationMetadata({ + lobby_id: lobby.id, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord returns 404 NotFound for an unknown message; may + // surface as 403 Forbidden if the bot lacks visibility, or + // BadRequest depending on which validation fires first. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ); + }), + ); + }); + + it("error - BadRequest for malformed (non-snowflake) message_id", async () => { + await runEffect( + Effect.gen(function* () { + const lobby = yield* createLobby({ idle_timeout_seconds: 5 }); + return yield* updateLobbyMessageExternalModerationMetadata({ + lobby_id: lobby.id, + message_id: "not-a-snowflake", + }).pipe( + Effect.flip, + Effect.map((e) => { + // Discord rejects malformed snowflakes with 400 Invalid Form + // Body; routing layers may also classify the path as 404, or + // the bot may lack permission and receive 403. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ); + }), + ); + }); + + it("error - Forbidden when the bot is not a member of the lobby", async () => { + // Snowflake-shaped lobby_id the bot is not a member of typically yields + // 403 Forbidden, or 404 NotFound if the route 404s before the membership + // check. + await runEffect( + updateLobbyMessageExternalModerationMetadata({ + lobby_id: NON_EXISTENT_LOBBY_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateMessage.test.ts b/packages/discord/test/updateMessage.test.ts new file mode 100644 index 000000000..2befd2a59 --- /dev/null +++ b/packages/discord/test/updateMessage.test.ts @@ -0,0 +1,168 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createMessage } from "../src/operations/createMessage.ts"; +import { deleteMessage } from "../src/operations/deleteMessage.ts"; +import { updateMessage } from "../src/operations/updateMessage.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// Requires a text channel the bot can post to. Operators must supply +// DISCORD_TEST_CHANNEL_ID for the happy path. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifiers that should not match real resources. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; +const NON_EXISTENT_MESSAGE_ID = "100000000000000001"; + +describe("updateMessage", () => { + it( + "happy path - edits a freshly posted message", + async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the updateMessage happy path", + ); + } + const originalContent = `distilled-upd-orig-${testRunId}`; + const newContent = `distilled-upd-new-${testRunId}`; + await runEffect( + Effect.gen(function* () { + const msg = yield* createMessage({ + channel_id: TEST_CHANNEL_ID, + content: originalContent, + }); + return yield* Effect.gen(function* () { + const updated = yield* updateMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + content: newContent, + }); + return yield* Effect.sync(() => { + expect(updated.id).toBe(msg.id); + expect(updated.channel_id).toBe(TEST_CHANNEL_ID); + expect(updated.content).toBe(newContent); + expect(typeof updated.author.id).toBe("string"); + expect(typeof updated.timestamp).toBe("string"); + expect(typeof updated.edited_timestamp).toBe("string"); + expect(typeof updated.flags).toBe("number"); + }); + }).pipe( + Effect.ensuring( + deleteMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for non-existent message_id on a real channel", async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the NotFound test", + ); + } + // A snowflake-shaped message_id that does not exist on the channel + // yields 404 NotFound. Discord may also surface 403 Forbidden depending + // on which check fires first. + await runEffect( + updateMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + content: `distilled-nf-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it( + "error - BadRequest when content exceeds 2000 characters", + async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the BadRequest test", + ); + } + // Discord's per-message content limit is 2000 chars; 2001 chars + // triggers 400 Invalid Form Body. We need a real message for the + // route to actually validate the body, so create-then-update. + const tooLong = "a".repeat(2001); + await runEffect( + Effect.gen(function* () { + const msg = yield* createMessage({ + channel_id: TEST_CHANNEL_ID, + content: `distilled-br-orig-${testRunId}`, + }); + return yield* updateMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + content: tooLong, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + Effect.ensuring( + deleteMessage({ + channel_id: TEST_CHANNEL_ID, + message_id: msg.id, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - Forbidden when targeting a channel the bot cannot see", async () => { + // A snowflake-shaped channel_id the bot cannot see typically yields 403 + // Forbidden (50001 Missing Access), or 404 NotFound if the route 404s + // before the permission check. + await runEffect( + updateMessage({ + channel_id: NON_EXISTENT_CHANNEL_ID, + message_id: NON_EXISTENT_MESSAGE_ID, + content: `distilled-fb-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateMyApplication.test.ts b/packages/discord/test/updateMyApplication.test.ts new file mode 100644 index 000000000..5a15419d9 --- /dev/null +++ b/packages/discord/test/updateMyApplication.test.ts @@ -0,0 +1,163 @@ +import { config } from "dotenv"; +import { Effect, Layer, Redacted } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getMyApplication } from "../src/operations/getMyApplication.ts"; +import { updateMyApplication } from "../src/operations/updateMyApplication.ts"; +import { + Credentials, + CredentialsFromEnv, + DEFAULT_API_BASE_URL, +} from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// A layer that supplies plausible-looking but invalid credentials so the +// /applications/@me endpoint rejects the request. The endpoint has no input +// path/query params, so error cases (NotFound / Forbidden) can only be +// reached by manipulating the auth context. +const bogusCredentialsLayer = ( + scheme: "Bot" | "Bearer", +): Layer.Layer => + Layer.succeed(Credentials, { + token: Redacted.make( + "MTAwMDAwMDAwMDAwMDAwMDAw.bogus.token-for-distilled-tests", + ), + authScheme: scheme, + apiBaseUrl: process.env.DISCORD_API_BASE_URL ?? DEFAULT_API_BASE_URL, + }); + +const runWithBogusCreds = ( + effect: Effect.Effect, + scheme: "Bot" | "Bearer", +): Promise => { + const layer = Layer.merge( + bogusCredentialsLayer(scheme), + FetchHttpClient.layer, + ); + return Effect.runPromise( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + effect.pipe(Effect.provide(layer)) as Effect.Effect, + ); +}; + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +describe("updateMyApplication", () => { + it( + "happy path - re-applies the bot application's existing description", + async () => { + // PATCH /applications/@me mutates persistent state on the bot + // application. To stay idempotent we snapshot the current description + // first, PATCH with that same value, then PATCH it back in the + // ensuring block in case the response mutates anything else. + await runEffect( + Effect.gen(function* () { + const before = yield* getMyApplication({}); + const original = before.description; + return yield* Effect.gen(function* () { + const updated = yield* updateMyApplication({ + description: { default: original }, + }); + return yield* Effect.sync(() => { + expect(updated.id).toBe(before.id); + expect(typeof updated.name).toBe("string"); + expect(updated.description).toBe(original); + expect(typeof updated.verify_key).toBe("string"); + expect(typeof updated.flags).toBe("number"); + expect(typeof updated.flags_new).toBe("string"); + expect(Array.isArray(updated.redirect_uris)).toBe(true); + expect(typeof updated.owner.id).toBe("string"); + }); + }).pipe( + Effect.ensuring( + updateMyApplication({ + description: { default: original }, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it( + "error - BadRequest when interactions_endpoint_url is malformed", + async () => { + // Discord requires interactions_endpoint_url to be a valid HTTPS URL + // and additionally validates it by issuing a PING. A clearly + // malformed value such as "not-a-url" yields 400 Invalid Form Body. + await runEffect( + updateMyApplication({ + interactions_endpoint_url: `not-a-url-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }, + 30_000, + ); + + it("error - Forbidden / NotFound surface when a Bot token is rejected", async () => { + // /applications/@me has no path params — the only way to reach the + // declared typed errors is to send an unrecognized token. Discord + // commonly returns 401 (mapped to Unauthorized) for invalid tokens, + // but may also classify the response as 403/404 depending on routing. + await runWithBogusCreds( + updateMyApplication({ + description: { default: `distilled-bogus-${testRunId}` }, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "NotFound", + "Forbidden", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + "Bot", + ); + }); + + it("error - Forbidden when a Bearer token is used on a bot-only route", async () => { + // /applications/@me is a bot-token-only route. Calling it with a Bearer + // credential typically yields 403 Forbidden, but Discord may also + // return 401 Unauthorized depending on token validity, or 404 if route + // resolution falls through ahead of the permission check. + await runWithBogusCreds( + updateMyApplication({ + description: { default: `distilled-bogus-${testRunId}` }, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "Forbidden", + "NotFound", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + "Bearer", + ); + }); +}); diff --git a/packages/discord/test/updateMyGuildMember.test.ts b/packages/discord/test/updateMyGuildMember.test.ts new file mode 100644 index 000000000..89c87c716 --- /dev/null +++ b/packages/discord/test/updateMyGuildMember.test.ts @@ -0,0 +1,147 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { updateMyGuildMember } from "../src/operations/updateMyGuildMember.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PATCH /guilds/{guild_id}/members/@me — the bot updates its own member +// object in a guild. The bot must be a member of the guild and have +// CHANGE_NICKNAME (for nick edits). +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifier that should not match a real guild. +const NON_EXISTENT_GUILD_ID = "100000000000000000"; + +// Discord nicknames must be 1–32 characters; build short, run-scoped values. +const nickFor = (suffix: string): string => + `dt_${suffix}_${testRunId}`.slice(0, 32); + +describe("updateMyGuildMember", () => { + it( + "happy path - updates the bot's own nickname in the test guild", + async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the updateMyGuildMember happy path", + ); + } + const newNick = nickFor("happy"); + await runEffect( + Effect.gen(function* () { + const updated = yield* updateMyGuildMember({ + guild_id: TEST_GUILD_ID, + nick: newNick, + }); + return yield* Effect.sync(() => { + expect(updated.nick).toBe(newNick); + expect(typeof updated.user.id).toBe("string"); + expect(typeof updated.user.username).toBe("string"); + expect(Array.isArray(updated.roles)).toBe(true); + expect(typeof updated.joined_at).toBe("string"); + expect(typeof updated.flags).toBe("number"); + expect(typeof updated.pending).toBe("boolean"); + expect(typeof updated.mute).toBe("boolean"); + expect(typeof updated.deaf).toBe("boolean"); + }).pipe( + // Restore the nickname to null so the test guild is not left in + // a dirtied state between runs. + Effect.ensuring( + updateMyGuildMember({ + guild_id: TEST_GUILD_ID, + nick: null, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - BadRequest for nickname exceeding 32 characters", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the BadRequest test", + ); + } + // Discord nicknames must be 1–32 characters; a 64-character value is + // rejected with 400 Invalid Form Body. + const tooLongNick = "a".repeat(64); + await runEffect( + updateMyGuildMember({ + guild_id: TEST_GUILD_ID, + nick: tooLongNick, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + // If the API somehow accepted the nick, restore it to null so the + // bot's display name in the test guild is not left dirty. + Effect.ensuring( + updateMyGuildMember({ + guild_id: TEST_GUILD_ID, + nick: null, + }).pipe(Effect.ignore), + ), + ), + ); + }); + + it("error - NotFound when the guild does not exist", async () => { + // A snowflake-shaped guild_id that resolves to no real guild typically + // yields 404 NotFound, but Discord may also classify the response as + // 403 Forbidden if the route 403s before the not-found check. + await runEffect( + updateMyGuildMember({ + guild_id: NON_EXISTENT_GUILD_ID, + nick: nickFor("nf"), + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot is not a member of", async () => { + // The same snowflake-shaped guild_id is also used to assert the + // Forbidden mapping: Discord typically returns 403 Forbidden (50001 + // Missing Access) for guilds the bot cannot see, or 404 NotFound if + // the route 404s before the permission check fires. + await runEffect( + updateMyGuildMember({ + guild_id: NON_EXISTENT_GUILD_ID, + bio: `distilled-fb-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateMyUser.test.ts b/packages/discord/test/updateMyUser.test.ts new file mode 100644 index 000000000..643685e52 --- /dev/null +++ b/packages/discord/test/updateMyUser.test.ts @@ -0,0 +1,155 @@ +import { config } from "dotenv"; +import { Effect, Layer, Redacted } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getMyUser } from "../src/operations/getMyUser.ts"; +import { updateMyUser } from "../src/operations/updateMyUser.ts"; +import { + Credentials, + CredentialsFromEnv, + DEFAULT_API_BASE_URL, +} from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +// A layer that supplies plausible-looking but invalid credentials so the +// /users/@me endpoint rejects the request. The endpoint has no input +// path/query params, so error cases (NotFound / Forbidden) can only be +// reached by manipulating the auth context. +const bogusCredentialsLayer = ( + scheme: "Bot" | "Bearer", +): Layer.Layer => + Layer.succeed(Credentials, { + token: Redacted.make( + "MTAwMDAwMDAwMDAwMDAwMDAw.bogus.token-for-distilled-tests", + ), + authScheme: scheme, + apiBaseUrl: process.env.DISCORD_API_BASE_URL ?? DEFAULT_API_BASE_URL, + }); + +const runWithBogusCreds = ( + effect: Effect.Effect, + scheme: "Bot" | "Bearer", +): Promise => { + const layer = Layer.merge( + bogusCredentialsLayer(scheme), + FetchHttpClient.layer, + ); + return Effect.runPromise( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + effect.pipe(Effect.provide(layer)) as Effect.Effect, + ); +}; + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +describe("updateMyUser", () => { + it( + "happy path - re-applies the bot user's existing username", + async () => { + // PATCH /users/@me mutates persistent state on the bot user object. + // To stay idempotent we snapshot the current username first, PATCH + // with that same value, then PATCH it back in the ensuring block. + // Note: bot users have a 2-per-hour rename rate limit, so we never + // change the actual value. + await runEffect( + Effect.gen(function* () { + const before = yield* getMyUser({}); + const original = before.username; + return yield* Effect.gen(function* () { + const updated = yield* updateMyUser({ username: original }); + return yield* Effect.sync(() => { + expect(updated.id).toBe(before.id); + expect(updated.username).toBe(original); + expect(typeof updated.discriminator).toBe("string"); + expect(typeof updated.public_flags).toBe("number"); + expect(typeof updated.flags).toBe("number"); + expect(typeof updated.mfa_enabled).toBe("boolean"); + expect( + updated.avatar === null || typeof updated.avatar === "string", + ).toBe(true); + }); + }).pipe( + Effect.ensuring( + updateMyUser({ username: original }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - BadRequest for usernames containing disallowed characters", async () => { + // Discord usernames must use only certain characters (lowercased + // letters, digits, underscores, periods). A value containing a hash + // and a discriminator-style suffix violates the username pattern and + // is rejected with 400 Invalid Form Body. + await runEffect( + updateMyUser({ + username: `Distilled Bad Name #${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden / NotFound surface when a Bot token is rejected", async () => { + // /users/@me has no path params — the only way to reach the declared + // typed errors is to send an unrecognized token. Discord commonly + // returns 401 (mapped to Unauthorized) for invalid tokens, but may + // also classify the response as 403/404 depending on routing. + await runWithBogusCreds( + updateMyUser({ username: `distilled_bogus_${testRunId}` }).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "NotFound", + "Forbidden", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + "Bot", + ); + }); + + it("error - Forbidden when a Bearer token without identify scope is used", async () => { + // /users/@me PATCH requires a Bot token (or a Bearer with identify + // scope). A bogus Bearer credential typically yields 403 Forbidden, + // but Discord may also return 401 Unauthorized depending on token + // validity, or 404 if route resolution falls through ahead of the + // permission check. + await runWithBogusCreds( + updateMyUser({ username: `distilled_bogus_${testRunId}` }).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "Forbidden", + "NotFound", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + "Bearer", + ); + }); +}); diff --git a/packages/discord/test/updateOriginalWebhookMessage.test.ts b/packages/discord/test/updateOriginalWebhookMessage.test.ts new file mode 100644 index 000000000..a18d8d952 --- /dev/null +++ b/packages/discord/test/updateOriginalWebhookMessage.test.ts @@ -0,0 +1,206 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { getOriginalWebhookMessage } from "../src/operations/getOriginalWebhookMessage.ts"; +import { updateOriginalWebhookMessage } from "../src/operations/updateOriginalWebhookMessage.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PATCH /webhooks/{webhook_id}/{webhook_token}/messages/@original — edits +// the original message for a webhook execution / interaction. Webhook +// routes authenticate purely via the (id, token) tuple in the path; no +// bot Authorization header is used. +// +// The happy path requires operator-supplied env vars pointing at a +// webhook that has at least one message posted via that token (so +// `@original` resolves). +const TEST_WEBHOOK_ID = process.env.DISCORD_TEST_WEBHOOK_ID; +const TEST_WEBHOOK_TOKEN = process.env.DISCORD_TEST_WEBHOOK_TOKEN; + +// Snowflake-shaped id and a randomly-generated token unlikely to match +// any real webhook. +const NON_EXISTENT_WEBHOOK_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const NON_EXISTENT_WEBHOOK_TOKEN = `distilled-bogus-webhook-token-${testRunId}`; + +describe("updateOriginalWebhookMessage", () => { + it( + "happy path - edits the original webhook message and restores its content", + async () => { + if (!TEST_WEBHOOK_ID || !TEST_WEBHOOK_TOKEN) { + throw new Error( + "DISCORD_TEST_WEBHOOK_ID and DISCORD_TEST_WEBHOOK_TOKEN must be set " + + "for the updateOriginalWebhookMessage happy path. The webhook " + + "must have at least one message posted via its token.", + ); + } + const newContent = `distilled-orig-upd-${testRunId}`; + await runEffect( + Effect.gen(function* () { + // Snapshot the original content so we can restore it in cleanup. + const before = yield* getOriginalWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: TEST_WEBHOOK_TOKEN, + }); + const originalContent = before.content; + return yield* Effect.gen(function* () { + const updated = yield* updateOriginalWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: TEST_WEBHOOK_TOKEN, + content: newContent, + }); + return yield* Effect.sync(() => { + expect(updated.id).toBe(before.id); + expect(updated.channel_id).toBe(before.channel_id); + expect(updated.content).toBe(newContent); + expect(typeof updated.timestamp).toBe("string"); + expect(typeof updated.edited_timestamp).toBe("string"); + expect(typeof updated.flags).toBe("number"); + expect(Array.isArray(updated.mentions)).toBe(true); + expect(Array.isArray(updated.mention_roles)).toBe(true); + expect(Array.isArray(updated.attachments)).toBe(true); + expect(Array.isArray(updated.embeds)).toBe(true); + expect(Array.isArray(updated.components)).toBe(true); + expect(typeof updated.author.id).toBe("string"); + }); + }).pipe( + // Restore the original content so the webhook's @original + // message is not left dirtied between runs. + Effect.ensuring( + updateOriginalWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: TEST_WEBHOOK_TOKEN, + content: originalContent, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it( + "error - BadRequest when content exceeds 2000 characters", + async () => { + if (!TEST_WEBHOOK_ID || !TEST_WEBHOOK_TOKEN) { + throw new Error( + "DISCORD_TEST_WEBHOOK_ID and DISCORD_TEST_WEBHOOK_TOKEN env vars " + + "are required for the BadRequest test", + ); + } + // Discord's per-message content limit is 2000 chars; 2001 chars + // triggers 400 Invalid Form Body. Snapshot the original content so + // that even though the PATCH is rejected, no state is left dirtied. + const tooLong = "a".repeat(2001); + await runEffect( + Effect.gen(function* () { + const before = yield* getOriginalWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: TEST_WEBHOOK_TOKEN, + }); + const originalContent = before.content; + return yield* updateOriginalWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: TEST_WEBHOOK_TOKEN, + content: tooLong, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + Effect.ensuring( + updateOriginalWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: TEST_WEBHOOK_TOKEN, + content: originalContent, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for a non-existent webhook id", async () => { + // Discord returns 404 (10015) for missing webhook ids. Token mismatch + // on a real id may surface as 401/403 instead. + await runEffect( + updateOriginalWebhookMessage({ + webhook_id: NON_EXISTENT_WEBHOOK_ID, + webhook_token: NON_EXISTENT_WEBHOOK_TOKEN, + content: `distilled-nf-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for a token mismatch on a real webhook id", async () => { + if (!TEST_WEBHOOK_ID) { + // Without a real webhook id, fall back to the missing-id case; + // Discord may classify this as Forbidden, NotFound, or Unauthorized + // depending on which check fires first. + await runEffect( + updateOriginalWebhookMessage({ + webhook_id: NON_EXISTENT_WEBHOOK_ID, + webhook_token: NON_EXISTENT_WEBHOOK_TOKEN, + content: `distilled-fb-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "Forbidden", + "NotFound", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + ); + return; + } + // Real webhook id + bogus token typically yields 401/403; some routes + // resolve as 404 instead. + await runEffect( + updateOriginalWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: NON_EXISTENT_WEBHOOK_TOKEN, + content: `distilled-fb-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "Forbidden", + "NotFound", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateSelfVoiceState.test.ts b/packages/discord/test/updateSelfVoiceState.test.ts new file mode 100644 index 000000000..16a7ced28 --- /dev/null +++ b/packages/discord/test/updateSelfVoiceState.test.ts @@ -0,0 +1,136 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { updateSelfVoiceState } from "../src/operations/updateSelfVoiceState.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PATCH /guilds/{guild_id}/voice-states/@me — updates the bot's own voice +// state in a guild stage channel. The bot must already be connected to +// the referenced stage channel; Discord returns 204 No Content on success +// (the SDK output is `Schema.Void`). +// +// The happy path therefore requires the operator to have the bot +// connected to a stage channel in DISCORD_TEST_STAGE_GUILD_ID, with the +// stage channel id supplied as DISCORD_TEST_STAGE_CHANNEL_ID. +const TEST_STAGE_GUILD_ID = process.env.DISCORD_TEST_STAGE_GUILD_ID; +const TEST_STAGE_CHANNEL_ID = process.env.DISCORD_TEST_STAGE_CHANNEL_ID; + +// Fallback guild id used for error tests that do not require the bot to +// be connected to a stage channel. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-shaped ids that should not match real resources. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const NON_EXISTENT_CHANNEL_ID = "100000000000000001"; + +describe("updateSelfVoiceState", () => { + it( + "happy path - moves the bot to audience (suppress=true) in its current stage channel", + async () => { + if (!TEST_STAGE_GUILD_ID || !TEST_STAGE_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_STAGE_GUILD_ID and DISCORD_TEST_STAGE_CHANNEL_ID " + + "must be set for the updateSelfVoiceState happy path. The bot " + + "must already be connected to the stage channel.", + ); + } + // Setting suppress=true moves the bot to the audience role in the + // stage. This is always available to the user themselves and is a + // safe idempotent operation. The endpoint returns 204 No Content, + // so the assertion is just that the call resolves without error. + await runEffect( + updateSelfVoiceState({ + guild_id: TEST_STAGE_GUILD_ID, + channel_id: TEST_STAGE_CHANNEL_ID, + suppress: true, + }).pipe( + Effect.map((result) => { + expect(result).toBeUndefined(); + }), + ), + ); + }, + 30_000, + ); + + it("error - BadRequest when channel_id is not a stage channel", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the BadRequest test", + ); + } + // Discord requires channel_id to point to a stage channel that the + // bot is currently connected to. A snowflake-shaped channel id that + // is not a stage channel (or that the bot is not connected to) is + // rejected with 400 Invalid Form Body / 50007-style errors. + await runEffect( + updateSelfVoiceState({ + guild_id: TEST_GUILD_ID, + channel_id: NON_EXISTENT_CHANNEL_ID, + suppress: true, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for a non-existent guild id", async () => { + // A snowflake-shaped guild_id that resolves to no real guild + // typically yields 404 NotFound (10004). Discord may also classify + // the response as 403 Forbidden if the route 403s before the + // not-found check, or BadRequest for malformed input. + await runEffect( + updateSelfVoiceState({ + guild_id: NON_EXISTENT_GUILD_ID, + suppress: true, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot cannot access", async () => { + // A guild_id the bot does not see typically yields 403 Forbidden + // (50001 Missing Access), or 404 NotFound if the route 404s before + // the permission check fires, or BadRequest for malformed input. + await runEffect( + updateSelfVoiceState({ + guild_id: NON_EXISTENT_GUILD_ID, + request_to_speak_timestamp: new Date().toISOString(), + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateStageInstance.test.ts b/packages/discord/test/updateStageInstance.test.ts new file mode 100644 index 000000000..cc9c4dcd4 --- /dev/null +++ b/packages/discord/test/updateStageInstance.test.ts @@ -0,0 +1,160 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createStageInstance } from "../src/operations/createStageInstance.ts"; +import { deleteStageInstance } from "../src/operations/deleteStageInstance.ts"; +import { updateStageInstance } from "../src/operations/updateStageInstance.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PATCH /stage-instances/{channel_id} — updates the live stage instance +// for a stage channel. Requires a stage channel (channel type 13) where +// the bot has MANAGE_CHANNELS / MUTE_MEMBERS / MOVE_MEMBERS and is a +// stage moderator. Operators must supply DISCORD_TEST_STAGE_CHANNEL_ID. +const TEST_STAGE_CHANNEL_ID = process.env.DISCORD_TEST_STAGE_CHANNEL_ID; + +// Snowflake-format identifier that should not match a real channel. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; + +// Discord requires a topic of 1..120 chars. +const stageTopic = (suffix: string): string => + `dt-upd-${suffix}-${testRunId}`.slice(0, 120); + +describe("updateStageInstance", () => { + it( + "happy path - creates a stage instance, updates its topic, and cleans up", + async () => { + if (!TEST_STAGE_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_STAGE_CHANNEL_ID env var is required for the updateStageInstance happy path (channel must be a stage channel)", + ); + } + const initialTopic = stageTopic("init"); + const newTopic = stageTopic("happy"); + await runEffect( + Effect.gen(function* () { + const created = yield* createStageInstance({ + channel_id: TEST_STAGE_CHANNEL_ID, + topic: initialTopic, + }); + return yield* Effect.gen(function* () { + const updated = yield* updateStageInstance({ + channel_id: TEST_STAGE_CHANNEL_ID, + topic: newTopic, + }); + return yield* Effect.sync(() => { + expect(updated.id).toBe(created.id); + expect(updated.channel_id).toBe(TEST_STAGE_CHANNEL_ID); + expect(updated.topic).toBe(newTopic); + expect(typeof updated.guild_id).toBe("string"); + expect(typeof updated.discoverable_disabled).toBe("boolean"); + }); + }).pipe( + Effect.ensuring( + deleteStageInstance({ + channel_id: TEST_STAGE_CHANNEL_ID, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it( + "error - BadRequest when topic exceeds 120 characters", + async () => { + if (!TEST_STAGE_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_STAGE_CHANNEL_ID env var is required for the BadRequest test", + ); + } + // Topic must be 1..120 characters; a 200-character value is + // rejected with 400 Invalid Form Body. Need a real live stage + // instance for the route to actually validate the body, so + // create-then-update. + const tooLongTopic = "a".repeat(200); + await runEffect( + Effect.gen(function* () { + yield* createStageInstance({ + channel_id: TEST_STAGE_CHANNEL_ID, + topic: stageTopic("br-init"), + }); + return yield* updateStageInstance({ + channel_id: TEST_STAGE_CHANNEL_ID, + topic: tooLongTopic, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + Effect.ensuring( + deleteStageInstance({ + channel_id: TEST_STAGE_CHANNEL_ID, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for a channel with no live stage instance", async () => { + // PATCH /stage-instances/{channel_id} returns 404 NotFound when the + // channel does not have a live stage instance, or when the channel + // does not exist. Discord may also surface 403 Forbidden if the bot + // lacks visibility, or BadRequest for malformed input. + await runEffect( + updateStageInstance({ + channel_id: NON_EXISTENT_CHANNEL_ID, + topic: stageTopic("nf"), + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a channel the bot cannot moderate", async () => { + // A snowflake-shaped channel_id the bot does not see typically yields + // 403 Forbidden (50001 Missing Access), or 404 NotFound if the route + // 404s before the permission check fires, or BadRequest for malformed + // input. + await runEffect( + updateStageInstance({ + channel_id: NON_EXISTENT_CHANNEL_ID, + topic: stageTopic("fb"), + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateUserMessageExternalModerationMetadata.test.ts b/packages/discord/test/updateUserMessageExternalModerationMetadata.test.ts new file mode 100644 index 000000000..685f70c55 --- /dev/null +++ b/packages/discord/test/updateUserMessageExternalModerationMetadata.test.ts @@ -0,0 +1,132 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { updateUserMessageExternalModerationMetadata } from "../src/operations/updateUserMessageExternalModerationMetadata.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PUT /partner-sdk/dms/{user_id_1}/{user_id_2}/messages/{message_id}/moderation-metadata +// — partner SDK endpoint that updates the external moderation metadata for +// a DM message between two users. Output is `Schema.Void`. There is no +// programmatic way to create a DM between two arbitrary test users from a +// bot, so the happy path requires operator-supplied env vars pointing at +// a DM message that the credential is allowed to moderate. +const TEST_USER_ID_1 = process.env.DISCORD_TEST_DM_USER_ID_1; +const TEST_USER_ID_2 = process.env.DISCORD_TEST_DM_USER_ID_2; +const TEST_DM_MESSAGE_ID = process.env.DISCORD_TEST_DM_MESSAGE_ID; + +// Snowflake-format identifiers that should not match real resources. +const NON_EXISTENT_USER_ID_1 = "100000000000000000"; +const NON_EXISTENT_USER_ID_2 = "100000000000000001"; +const NON_EXISTENT_MESSAGE_ID = "100000000000000002"; + +describe("updateUserMessageExternalModerationMetadata", () => { + it( + "happy path - PUT against a real DM message resolves with no body", + async () => { + if (!TEST_USER_ID_1 || !TEST_USER_ID_2 || !TEST_DM_MESSAGE_ID) { + throw new Error( + "DISCORD_TEST_DM_USER_ID_1, DISCORD_TEST_DM_USER_ID_2 and " + + "DISCORD_TEST_DM_MESSAGE_ID env vars are required for the " + + "updateUserMessageExternalModerationMetadata happy path. The " + + "credential must be authorised under the partner SDK to moderate " + + "this DM message.", + ); + } + await runEffect( + updateUserMessageExternalModerationMetadata({ + user_id_1: TEST_USER_ID_1, + user_id_2: TEST_USER_ID_2, + message_id: TEST_DM_MESSAGE_ID, + }).pipe( + Effect.map((result) => { + // Output schema is Void — successful resolution is the assertion. + expect(result).toBeUndefined(); + }), + ), + ); + }, + 30_000, + ); + + it("error - BadRequest for a malformed (non-snowflake) message_id", async () => { + // Discord rejects malformed snowflakes with 400 Invalid Form Body; + // routing layers may also classify the path as 404, or the partner + // SDK auth may receive 403 before the body is validated. + await runEffect( + updateUserMessageExternalModerationMetadata({ + user_id_1: NON_EXISTENT_USER_ID_1, + user_id_2: NON_EXISTENT_USER_ID_2, + message_id: `not-a-snowflake-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["BadRequest", "NotFound", "Forbidden"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for a non-existent message_id", async () => { + // Snowflake-shaped ids that resolve to no real DM message yield 404 + // NotFound. Discord may also classify the response as 403 Forbidden + // if the partner SDK auth check fires first, or BadRequest depending + // on validation order. + await runEffect( + updateUserMessageExternalModerationMetadata({ + user_id_1: NON_EXISTENT_USER_ID_1, + user_id_2: NON_EXISTENT_USER_ID_2, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when the credential is not authorised for the DM", async () => { + // The partner SDK route requires a credential authorised to moderate + // the targeted DM. A regular bot token typically yields 403 Forbidden + // (or 401 Unauthorized) on this route, but Discord may also return + // 404 NotFound if the route 404s before the auth check, or BadRequest + // for malformed input. + await runEffect( + updateUserMessageExternalModerationMetadata({ + user_id_1: NON_EXISTENT_USER_ID_1, + user_id_2: NON_EXISTENT_USER_ID_2, + message_id: NON_EXISTENT_MESSAGE_ID, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "Forbidden", + "NotFound", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateVoiceChannelStatus.test.ts b/packages/discord/test/updateVoiceChannelStatus.test.ts new file mode 100644 index 000000000..4fe042e9a --- /dev/null +++ b/packages/discord/test/updateVoiceChannelStatus.test.ts @@ -0,0 +1,144 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { updateVoiceChannelStatus } from "../src/operations/updateVoiceChannelStatus.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PUT /channels/{channel_id}/voice-status — sets the status text of a +// voice channel. Output is `Schema.Void` (204 No Content). Requires a +// voice channel (channel type 2) where the bot has the +// SET_VOICE_CHANNEL_STATUS permission. Operators must supply +// DISCORD_TEST_VOICE_CHANNEL_ID for the happy path. +const TEST_VOICE_CHANNEL_ID = process.env.DISCORD_TEST_VOICE_CHANNEL_ID; + +// Snowflake-format identifier that should not match a real channel. +const NON_EXISTENT_CHANNEL_ID = "100000000000000000"; + +describe("updateVoiceChannelStatus", () => { + it( + "happy path - sets and clears the voice channel status", + async () => { + if (!TEST_VOICE_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_VOICE_CHANNEL_ID env var is required for the updateVoiceChannelStatus happy path (must be a voice channel where the bot has SET_VOICE_CHANNEL_STATUS)", + ); + } + const status = `distilled-vcstatus-${testRunId}`; + await runEffect( + Effect.gen(function* () { + const result = yield* updateVoiceChannelStatus({ + channel_id: TEST_VOICE_CHANNEL_ID, + status, + }); + return yield* Effect.sync(() => { + // Output schema is Void — successful resolution is the assertion. + expect(result).toBeUndefined(); + }); + }).pipe( + // Restore the status to null so the test channel is not left in + // a dirtied state between runs. + Effect.ensuring( + updateVoiceChannelStatus({ + channel_id: TEST_VOICE_CHANNEL_ID, + status: null, + }).pipe(Effect.ignore), + ), + ), + ); + }, + 30_000, + ); + + it( + "error - BadRequest when status exceeds the 500-character limit", + async () => { + if (!TEST_VOICE_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_VOICE_CHANNEL_ID env var is required for the BadRequest test", + ); + } + // Discord's voice channel status limit is 500 chars; 501 chars is + // rejected with 400 Invalid Form Body. + const tooLong = "a".repeat(501); + await runEffect( + updateVoiceChannelStatus({ + channel_id: TEST_VOICE_CHANNEL_ID, + status: tooLong, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + // If the API somehow accepted the status, restore it to null so + // the channel is not left dirty. + Effect.ensuring( + updateVoiceChannelStatus({ + channel_id: TEST_VOICE_CHANNEL_ID, + status: null, + }).pipe(Effect.ignore), + ), + ), + ); + }, + 30_000, + ); + + it("error - NotFound for a non-existent channel id", async () => { + // A snowflake-shaped channel_id that resolves to no real channel + // typically yields 404 NotFound (10003). Discord may also classify + // the response as 403 Forbidden if the route 403s before the + // not-found check, or BadRequest for malformed input. + await runEffect( + updateVoiceChannelStatus({ + channel_id: NON_EXISTENT_CHANNEL_ID, + status: `distilled-nf-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a channel the bot cannot access", async () => { + // A snowflake-shaped channel_id the bot does not see typically yields + // 403 Forbidden (50001 Missing Access), or 404 NotFound if the route + // 404s before the permission check fires. A non-voice channel + // returns 400 BadRequest because the route only accepts voice + // channels. + await runEffect( + updateVoiceChannelStatus({ + channel_id: NON_EXISTENT_CHANNEL_ID, + status: `distilled-fb-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateVoiceState.test.ts b/packages/discord/test/updateVoiceState.test.ts new file mode 100644 index 000000000..5fa37d53c --- /dev/null +++ b/packages/discord/test/updateVoiceState.test.ts @@ -0,0 +1,153 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { updateVoiceState } from "../src/operations/updateVoiceState.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PATCH /guilds/{guild_id}/voice-states/{user_id} — updates another +// user's voice state in a guild stage channel. The target user must +// already be connected to the referenced stage channel; the bot needs +// MUTE_MEMBERS in that channel. Discord returns 204 No Content on +// success (the SDK output is `Schema.Void`). +// +// The happy path requires the operator to have a target user connected +// to a stage channel that the bot can moderate, supplied via: +// - DISCORD_TEST_STAGE_GUILD_ID +// - DISCORD_TEST_STAGE_CHANNEL_ID +// - DISCORD_TEST_STAGE_TARGET_USER_ID (must currently be in the stage +// channel as audience so suppress=true is idempotent) +const TEST_STAGE_GUILD_ID = process.env.DISCORD_TEST_STAGE_GUILD_ID; +const TEST_STAGE_CHANNEL_ID = process.env.DISCORD_TEST_STAGE_CHANNEL_ID; +const TEST_STAGE_TARGET_USER_ID = + process.env.DISCORD_TEST_STAGE_TARGET_USER_ID; + +// Fallback guild id used for error tests that do not require the target +// user to be connected to a stage channel. +const TEST_GUILD_ID = process.env.DISCORD_TEST_GUILD_ID; + +// Snowflake-format identifiers that should not match real resources. +const NON_EXISTENT_GUILD_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const NON_EXISTENT_USER_ID = "100000000000000001"; +const NON_EXISTENT_CHANNEL_ID = "100000000000000002"; + +describe("updateVoiceState", () => { + it( + "happy path - keeps the target user suppressed (audience) in the stage channel", + async () => { + if ( + !TEST_STAGE_GUILD_ID || + !TEST_STAGE_CHANNEL_ID || + !TEST_STAGE_TARGET_USER_ID + ) { + throw new Error( + "DISCORD_TEST_STAGE_GUILD_ID, DISCORD_TEST_STAGE_CHANNEL_ID and " + + "DISCORD_TEST_STAGE_TARGET_USER_ID must be set for the " + + "updateVoiceState happy path. The target user must currently be " + + "connected to the stage channel.", + ); + } + // Setting suppress=true keeps the target user in the audience role. + // This is idempotent for an audience user and is the safe action + // available to a bot with MUTE_MEMBERS. Output is Void, so the + // assertion is just that the call resolves without error. + await runEffect( + updateVoiceState({ + guild_id: TEST_STAGE_GUILD_ID, + user_id: TEST_STAGE_TARGET_USER_ID, + channel_id: TEST_STAGE_CHANNEL_ID, + suppress: true, + }).pipe( + Effect.map((result) => { + expect(result).toBeUndefined(); + }), + ), + ); + }, + 30_000, + ); + + it("error - BadRequest when channel_id is not a stage channel the user is in", async () => { + if (!TEST_GUILD_ID) { + throw new Error( + "DISCORD_TEST_GUILD_ID env var is required for the BadRequest test", + ); + } + // Discord requires channel_id to point to a stage channel that the + // target user is currently connected to. A snowflake-shaped channel + // id that is not a stage channel (or that the user is not in) is + // rejected with 400 Invalid Form Body. + await runEffect( + updateVoiceState({ + guild_id: TEST_GUILD_ID, + user_id: NON_EXISTENT_USER_ID, + channel_id: NON_EXISTENT_CHANNEL_ID, + suppress: true, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - NotFound for a non-existent guild id", async () => { + // A snowflake-shaped guild_id that resolves to no real guild + // typically yields 404 NotFound (10004). Discord may also classify + // the response as 403 Forbidden if the route 403s before the + // not-found check, or BadRequest for malformed input. + await runEffect( + updateVoiceState({ + guild_id: NON_EXISTENT_GUILD_ID, + user_id: NON_EXISTENT_USER_ID, + suppress: true, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a guild the bot cannot access", async () => { + // A guild_id the bot does not see typically yields 403 Forbidden + // (50001 Missing Access), or 404 NotFound if the route 404s before + // the permission check fires, or BadRequest for malformed input. + await runEffect( + updateVoiceState({ + guild_id: NON_EXISTENT_GUILD_ID, + user_id: NON_EXISTENT_USER_ID, + channel_id: NON_EXISTENT_CHANNEL_ID, + suppress: false, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateWebhook.test.ts b/packages/discord/test/updateWebhook.test.ts new file mode 100644 index 000000000..07e0aac77 --- /dev/null +++ b/packages/discord/test/updateWebhook.test.ts @@ -0,0 +1,161 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { createWebhook } from "../src/operations/createWebhook.ts"; +import { deleteWebhook } from "../src/operations/deleteWebhook.ts"; +import { updateWebhook } from "../src/operations/updateWebhook.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PATCH /webhooks/{webhook_id} — updates a webhook's name / avatar / +// channel_id. Requires a text/announcement/forum channel where the bot +// has MANAGE_WEBHOOKS. Operators must supply DISCORD_TEST_CHANNEL_ID for +// the happy path so the test can create + update + delete its own +// webhook. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-format identifier that should not match a real webhook. +const NON_EXISTENT_WEBHOOK_ID = "100000000000000000"; + +// Discord requires webhook names of 1..80 chars and disallows certain +// substrings ("clyde", "discord"). +const webhookName = (suffix: string): string => + `dt-upd-${suffix}-${testRunId}`.slice(0, 80); + +describe("updateWebhook", () => { + it( + "happy path - creates a webhook, renames it, and cleans up", + async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the updateWebhook happy path", + ); + } + const initialName = webhookName("init"); + const newName = webhookName("happy"); + await runEffect( + Effect.gen(function* () { + const webhook = yield* createWebhook({ + channel_id: TEST_CHANNEL_ID, + name: initialName, + }); + return yield* Effect.gen(function* () { + const updated = yield* updateWebhook({ + webhook_id: webhook.id, + name: newName, + }); + return yield* Effect.sync(() => { + // Output is opaque on the schema; Discord returns the + // updated webhook object. Narrow the shape for assertions. + const obj = updated as { + id?: string; + name?: string; + channel_id?: string; + }; + expect(obj.id).toBe(webhook.id); + expect(obj.name).toBe(newName); + expect(typeof obj.channel_id).toBe("string"); + }); + }).pipe( + Effect.ensuring( + deleteWebhook({ webhook_id: webhook.id }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it( + "error - BadRequest when name is empty", + async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the BadRequest test", + ); + } + // Webhook name must be 1..80 chars; empty string is rejected with + // 400 Invalid Form Body. Need a real webhook for the route to + // actually validate the body, so create-then-update. + await runEffect( + Effect.gen(function* () { + const webhook = yield* createWebhook({ + channel_id: TEST_CHANNEL_ID, + name: webhookName("br-init"), + }); + return yield* updateWebhook({ + webhook_id: webhook.id, + name: "", + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + Effect.ensuring( + deleteWebhook({ webhook_id: webhook.id }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for a non-existent webhook id", async () => { + // Discord returns 404 NotFound (10015) for missing webhook ids. The + // bot's auth context may also classify the response as 403 Forbidden + // if the route 403s before the not-found check, or BadRequest for + // malformed input. + await runEffect( + updateWebhook({ + webhook_id: NON_EXISTENT_WEBHOOK_ID, + name: webhookName("nf"), + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting a webhook the bot cannot manage", async () => { + // A snowflake-shaped webhook_id the bot cannot see typically yields + // 403 Forbidden (50001 Missing Access), or 404 NotFound if the route + // 404s before the permission check fires, or BadRequest for + // malformed input. + await runEffect( + updateWebhook({ + webhook_id: NON_EXISTENT_WEBHOOK_ID, + name: webhookName("fb"), + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateWebhookByToken.test.ts b/packages/discord/test/updateWebhookByToken.test.ts new file mode 100644 index 000000000..008471b37 --- /dev/null +++ b/packages/discord/test/updateWebhookByToken.test.ts @@ -0,0 +1,193 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { createWebhook } from "../src/operations/createWebhook.ts"; +import { deleteWebhook } from "../src/operations/deleteWebhook.ts"; +import { updateWebhookByToken } from "../src/operations/updateWebhookByToken.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PATCH /webhooks/{webhook_id}/{webhook_token} updates a webhook using +// its token. The route does not require bot auth — the token in the URL +// is the credential. The happy path creates a webhook in an +// operator-supplied text channel (DISCORD_TEST_CHANNEL_ID), reads its +// returned token, renames it via the token route, and deletes the +// webhook on cleanup. +const TEST_CHANNEL_ID = process.env.DISCORD_TEST_CHANNEL_ID; + +// Snowflake-shaped ids unlikely to resolve to any real webhook, plus a +// plausible-looking but invalid token. +const NON_EXISTENT_WEBHOOK_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const INACCESSIBLE_WEBHOOK_ID = "100000000000000001"; +const BOGUS_WEBHOOK_TOKEN = `distilled-bogus-token-${testRunId}`; + +// Discord requires webhook names of 1..80 chars and disallows certain +// substrings ("clyde", "discord"). +const webhookName = (suffix: string): string => + `dt-upd-tok-${suffix}-${testRunId}`.slice(0, 80); + +describe("updateWebhookByToken", () => { + it( + "happy path - renames a webhook via its token", + async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the updateWebhookByToken happy path. " + + "Set it to a text channel id where the bot has MANAGE_WEBHOOKS.", + ); + } + const initialName = webhookName("init"); + const newName = webhookName("happy"); + const created = await runEffect( + createWebhook({ channel_id: TEST_CHANNEL_ID, name: initialName }), + ); + const webhookId = created.id; + const webhookToken = created.token; + if (!webhookToken) { + // Defensive: incoming webhooks always include a token, but if the + // server omits it we cannot exercise the token route — fail + // loudly so the operator can investigate, then clean up. + await runEffect( + deleteWebhook({ webhook_id: webhookId }).pipe(Effect.ignore), + ); + throw new Error( + "createWebhook did not return a token; cannot exercise updateWebhookByToken happy path.", + ); + } + try { + const result = await runEffect( + updateWebhookByToken({ + webhook_id: webhookId, + webhook_token: webhookToken, + name: newName, + }), + ); + // Output is opaque on the schema; Discord returns the updated + // webhook object. Narrow the shape for assertions. + const webhook = result as { + id?: string; + name?: string; + channel_id?: string; + }; + expect(webhook.id).toBe(webhookId); + expect(webhook.name).toBe(newName); + expect(typeof webhook.channel_id).toBe("string"); + } finally { + await runEffect( + deleteWebhook({ webhook_id: webhookId }).pipe(Effect.ignore), + ); + } + }, + { timeout: 30_000 }, + ); + + it( + "error - BadRequest when name is empty", + async () => { + if (!TEST_CHANNEL_ID) { + throw new Error( + "DISCORD_TEST_CHANNEL_ID env var is required for the BadRequest test", + ); + } + // Webhook name must be 1..80 chars; empty string is rejected with + // 400 Invalid Form Body. Need a real webhook (with a real token) + // for the route to actually validate the body. + const created = await runEffect( + createWebhook({ + channel_id: TEST_CHANNEL_ID, + name: webhookName("br-init"), + }), + ); + const webhookId = created.id; + const webhookToken = created.token; + if (!webhookToken) { + await runEffect( + deleteWebhook({ webhook_id: webhookId }).pipe(Effect.ignore), + ); + throw new Error( + "createWebhook did not return a token; cannot exercise updateWebhookByToken BadRequest test.", + ); + } + try { + await runEffect( + updateWebhookByToken({ + webhook_id: webhookId, + webhook_token: webhookToken, + name: "", + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + } finally { + await runEffect( + deleteWebhook({ webhook_id: webhookId }).pipe(Effect.ignore), + ); + } + }, + { timeout: 30_000 }, + ); + + it("error - NotFound for a non-existent webhook id", async () => { + // Discord returns 404 NotFound (10015) for missing webhook ids. Some + // malformed or out-of-range snowflakes may surface as BadRequest; + // the bogus token may also yield Forbidden. + await runEffect( + updateWebhookByToken({ + webhook_id: NON_EXISTENT_WEBHOOK_ID, + webhook_token: BOGUS_WEBHOOK_TOKEN, + name: webhookName("nf"), + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when the (id, token) pair does not match a real webhook", async () => { + // Webhook routes authenticate purely via the (id, token) tuple. A + // bogus pair fails authentication. Discord typically returns 401 / + // 403 mapped to Forbidden, or 404 NotFound to avoid leaking + // existence; some malformed snowflakes may surface as BadRequest. + await runEffect( + updateWebhookByToken({ + webhook_id: INACCESSIBLE_WEBHOOK_ID, + webhook_token: BOGUS_WEBHOOK_TOKEN, + name: webhookName("fb"), + }).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "Forbidden", + "NotFound", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/updateWebhookMessage.test.ts b/packages/discord/test/updateWebhookMessage.test.ts new file mode 100644 index 000000000..4e6cfdcdb --- /dev/null +++ b/packages/discord/test/updateWebhookMessage.test.ts @@ -0,0 +1,220 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { CredentialsFromEnv } from "../src/credentials.ts"; +import { getWebhookMessage } from "../src/operations/getWebhookMessage.ts"; +import { updateWebhookMessage } from "../src/operations/updateWebhookMessage.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// PATCH /webhooks/{webhook_id}/{webhook_token}/messages/{message_id} — +// edits a specific message previously sent by the webhook. The route is +// authenticated purely via the (id, token) pair in the path; no bot +// Authorization header is used. +// +// The happy path requires operator-supplied env vars pointing at a +// webhook and a known message id posted via that webhook. +const TEST_WEBHOOK_ID = process.env.DISCORD_TEST_WEBHOOK_ID; +const TEST_WEBHOOK_TOKEN = process.env.DISCORD_TEST_WEBHOOK_TOKEN; +const TEST_WEBHOOK_MESSAGE_ID = process.env.DISCORD_TEST_WEBHOOK_MESSAGE_ID; + +// Snowflake-shaped ids and a randomly-generated token unlikely to match +// any real webhook. +const NON_EXISTENT_WEBHOOK_ID = `1000000000000000${testRunId.slice(0, 2)}`; +const NON_EXISTENT_WEBHOOK_TOKEN = `distilled-bogus-webhook-token-${testRunId}`; +const NON_EXISTENT_MESSAGE_ID = "100000000000000002"; + +describe("updateWebhookMessage", () => { + it( + "happy path - edits a webhook message and restores its content", + async () => { + if (!TEST_WEBHOOK_ID || !TEST_WEBHOOK_TOKEN || !TEST_WEBHOOK_MESSAGE_ID) { + throw new Error( + "DISCORD_TEST_WEBHOOK_ID, DISCORD_TEST_WEBHOOK_TOKEN, and " + + "DISCORD_TEST_WEBHOOK_MESSAGE_ID must be set for the " + + "updateWebhookMessage happy path. The message_id must reference " + + "a message posted via the webhook token.", + ); + } + const newContent = `distilled-wh-msg-upd-${testRunId}`; + await runEffect( + Effect.gen(function* () { + // Snapshot the current content so we can restore it in cleanup. + const before = yield* getWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: TEST_WEBHOOK_TOKEN, + message_id: TEST_WEBHOOK_MESSAGE_ID, + }); + const originalContent = before.content; + return yield* Effect.gen(function* () { + const updated = yield* updateWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: TEST_WEBHOOK_TOKEN, + message_id: TEST_WEBHOOK_MESSAGE_ID, + content: newContent, + }); + return yield* Effect.sync(() => { + expect(updated.id).toBe(TEST_WEBHOOK_MESSAGE_ID); + expect(updated.channel_id).toBe(before.channel_id); + expect(updated.content).toBe(newContent); + expect(typeof updated.timestamp).toBe("string"); + expect(typeof updated.edited_timestamp).toBe("string"); + expect(typeof updated.flags).toBe("number"); + expect(Array.isArray(updated.mentions)).toBe(true); + expect(Array.isArray(updated.mention_roles)).toBe(true); + expect(Array.isArray(updated.attachments)).toBe(true); + expect(Array.isArray(updated.embeds)).toBe(true); + expect(Array.isArray(updated.components)).toBe(true); + expect(typeof updated.author.id).toBe("string"); + }); + }).pipe( + // Restore the original content so the test message is not + // left dirtied between runs. + Effect.ensuring( + updateWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: TEST_WEBHOOK_TOKEN, + message_id: TEST_WEBHOOK_MESSAGE_ID, + content: originalContent, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it( + "error - BadRequest when content exceeds 2000 characters", + async () => { + if (!TEST_WEBHOOK_ID || !TEST_WEBHOOK_TOKEN || !TEST_WEBHOOK_MESSAGE_ID) { + throw new Error( + "DISCORD_TEST_WEBHOOK_ID, DISCORD_TEST_WEBHOOK_TOKEN and " + + "DISCORD_TEST_WEBHOOK_MESSAGE_ID env vars are required for " + + "the BadRequest test", + ); + } + // Discord's per-message content limit is 2000 chars; 2001 chars + // triggers 400 Invalid Form Body. Snapshot the original content so + // that even though the PATCH is rejected, no state is left dirtied + // if the API somehow accepted it. + const tooLong = "a".repeat(2001); + await runEffect( + Effect.gen(function* () { + const before = yield* getWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: TEST_WEBHOOK_TOKEN, + message_id: TEST_WEBHOOK_MESSAGE_ID, + }); + const originalContent = before.content; + return yield* updateWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: TEST_WEBHOOK_TOKEN, + message_id: TEST_WEBHOOK_MESSAGE_ID, + content: tooLong, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + Effect.ensuring( + updateWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: TEST_WEBHOOK_TOKEN, + message_id: TEST_WEBHOOK_MESSAGE_ID, + content: originalContent, + }).pipe(Effect.ignore), + ), + ); + }), + ); + }, + 30_000, + ); + + it("error - NotFound for a non-existent webhook id", async () => { + // Discord returns 404 (10015) for missing webhook ids. Token + // mismatch on a real id may surface as 401/403 instead, and + // malformed snowflakes as BadRequest. + await runEffect( + updateWebhookMessage({ + webhook_id: NON_EXISTENT_WEBHOOK_ID, + webhook_token: NON_EXISTENT_WEBHOOK_TOKEN, + message_id: NON_EXISTENT_MESSAGE_ID, + content: `distilled-nf-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden for a token mismatch on a real webhook id", async () => { + if (!TEST_WEBHOOK_ID) { + // Without a real webhook id, fall back to the missing-id case; + // Discord may classify as Forbidden, NotFound, Unauthorized, or + // BadRequest depending on which check fires first. + await runEffect( + updateWebhookMessage({ + webhook_id: NON_EXISTENT_WEBHOOK_ID, + webhook_token: NON_EXISTENT_WEBHOOK_TOKEN, + message_id: NON_EXISTENT_MESSAGE_ID, + content: `distilled-fb-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "Forbidden", + "NotFound", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + ); + return; + } + // Real webhook id + bogus token typically yields 401/403; some + // routes resolve as 404 instead. + await runEffect( + updateWebhookMessage({ + webhook_id: TEST_WEBHOOK_ID, + webhook_token: NON_EXISTENT_WEBHOOK_TOKEN, + message_id: NON_EXISTENT_MESSAGE_ID, + content: `distilled-fb-${testRunId}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect([ + "Forbidden", + "NotFound", + "Unauthorized", + "BadRequest", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ]).toContain((e as any)._tag); + }), + ), + ); + }); +}); diff --git a/packages/discord/test/uploadApplicationAttachment.test.ts b/packages/discord/test/uploadApplicationAttachment.test.ts new file mode 100644 index 000000000..b99032315 --- /dev/null +++ b/packages/discord/test/uploadApplicationAttachment.test.ts @@ -0,0 +1,152 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { describe, expect, it } from "vitest"; +import { uploadApplicationAttachment } from "../src/operations/uploadApplicationAttachment.ts"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +config(); + +const MainLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(MainLayer)) as Effect.Effect, + ); + +const testRunId: string = crypto.randomUUID().replace(/-/g, "").slice(0, 8); + +// POST /applications/{application_id}/attachment — uploads an ephemeral +// attachment that can be referenced from interaction responses / +// component messages. Body is multipart with a `file` field. The +// application_id must be the bot's own; the credential must be a Bot +// token. Returned attachments are CDN-hosted and ephemeral (no delete +// endpoint), so cleanup is not required. +// +// The happy path requires: +// - DISCORD_TEST_APPLICATION_ID — the bot's application id +// - DISCORD_TEST_ATTACHMENT_DATA_URI — a data URI for any small file +// (e.g. an image/png base64 payload) +const TEST_APPLICATION_ID = process.env.DISCORD_TEST_APPLICATION_ID; +const TEST_ATTACHMENT_DATA_URI = + process.env.DISCORD_TEST_ATTACHMENT_DATA_URI; + +// Snowflake-format identifier that should not match a real application. +const NON_EXISTENT_APPLICATION_ID = "100000000000000000"; + +// A clearly invalid file payload — an empty data URI — used for the +// BadRequest path; Discord rejects this with 400 Invalid Form Body. +const INVALID_ATTACHMENT_DATA_URI = "data:application/octet-stream;base64,"; + +describe("uploadApplicationAttachment", () => { + it( + "happy path - uploads an attachment and returns its CDN metadata", + async () => { + if (!TEST_APPLICATION_ID || !TEST_ATTACHMENT_DATA_URI) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID and DISCORD_TEST_ATTACHMENT_DATA_URI " + + "env vars are required for the uploadApplicationAttachment happy path", + ); + } + const result = await runEffect( + uploadApplicationAttachment({ + application_id: TEST_APPLICATION_ID, + file: TEST_ATTACHMENT_DATA_URI, + }), + ); + expect(typeof result.attachment.id).toBe("string"); + expect(result.attachment.id.length).toBeGreaterThan(0); + expect(typeof result.attachment.filename).toBe("string"); + expect(typeof result.attachment.size).toBe("number"); + expect(result.attachment.size).toBeGreaterThan(0); + expect(typeof result.attachment.url).toBe("string"); + expect(result.attachment.url.startsWith("http")).toBe(true); + expect(typeof result.attachment.proxy_url).toBe("string"); + // Test run id is captured here so failed uploads can be correlated + // back to a specific run via grep. + expect(testRunId).toMatch(/^[0-9a-f]{8}$/); + }, + 30_000, + ); + + it( + "error - BadRequest for an invalid (empty) attachment data URI", + async () => { + if (!TEST_APPLICATION_ID) { + throw new Error( + "DISCORD_TEST_APPLICATION_ID env var is required for the BadRequest test", + ); + } + // An empty / malformed file data URI is rejected with 400 Invalid + // Form Body. May also surface as Forbidden if the bot lacks access + // to the application, or NotFound for an unseen application id. + await runEffect( + uploadApplicationAttachment({ + application_id: TEST_APPLICATION_ID, + file: INVALID_ATTACHMENT_DATA_URI, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["BadRequest", "Forbidden", "NotFound"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }, + 30_000, + ); + + it("error - NotFound for a non-existent application id", async () => { + // A snowflake-shaped application_id that resolves to no real + // application typically yields 404 NotFound, but Discord may also + // classify the response as 403 Forbidden (the bot may only upload + // attachments for its own application), or BadRequest if validation + // fires first. + await runEffect( + uploadApplicationAttachment({ + application_id: NON_EXISTENT_APPLICATION_ID, + file: + TEST_ATTACHMENT_DATA_URI ?? + `data:text/plain;base64,${Buffer.from( + `distilled-nf-${testRunId}`, + ).toString("base64")}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["NotFound", "Forbidden", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); + + it("error - Forbidden when targeting an application id other than the bot's", async () => { + // The bot can only upload attachments under its own application. + // A snowflake-shaped application_id that does not match the bot's + // typically yields 403 Forbidden, or 404 NotFound if the route 404s + // before the ownership check, or BadRequest for malformed input. + await runEffect( + uploadApplicationAttachment({ + application_id: NON_EXISTENT_APPLICATION_ID, + file: + TEST_ATTACHMENT_DATA_URI ?? + `data:text/plain;base64,${Buffer.from( + `distilled-fb-${testRunId}`, + ).toString("base64")}`, + }).pipe( + Effect.flip, + Effect.map((e) => { + expect(["Forbidden", "NotFound", "BadRequest"]).toContain( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (e as any)._tag, + ); + }), + ), + ); + }); +}); diff --git a/packages/discord/tsconfig.json b/packages/discord/tsconfig.json new file mode 100644 index 000000000..3760701b6 --- /dev/null +++ b/packages/discord/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [ + "src/**/*.ts" + ], + "compilerOptions": { + "outDir": "./lib", + "rootDir": "./src", + "paths": { + "~/*": [ + "./src/*" + ] + } + }, + "references": [ + { + "path": "../core" + } + ] +} \ No newline at end of file diff --git a/packages/discord/tsconfig.test.json b/packages/discord/tsconfig.test.json new file mode 100644 index 000000000..b2af39b25 --- /dev/null +++ b/packages/discord/tsconfig.test.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ], + "compilerOptions": { + "rootDir": ".", + "noEmit": true, + "paths": { + "~/*": [ + "./src/*" + ] + } + } +} \ No newline at end of file diff --git a/packages/discord/vitest.config.ts b/packages/discord/vitest.config.ts new file mode 100644 index 000000000..fdad53402 --- /dev/null +++ b/packages/discord/vitest.config.ts @@ -0,0 +1,17 @@ +import { config } from "dotenv"; +import { resolve } from "path"; + +config({ path: resolve(__dirname, "../../.env") }); +config({ path: resolve(__dirname, ".env") }); + +export default { + test: { + include: ["test/**/*.test.ts"], + testTimeout: 120000, + }, + resolve: { + alias: { + "~": new URL("./src", import.meta.url).pathname, + }, + }, +};