@effect-uai/discord 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -0
  3. package/dist/Discord.d.mts +55 -0
  4. package/dist/Discord.d.mts.map +1 -0
  5. package/dist/Discord.mjs +196 -0
  6. package/dist/Discord.mjs.map +1 -0
  7. package/dist/events-Du1u-kVV.d.mts +172 -0
  8. package/dist/events-Du1u-kVV.d.mts.map +1 -0
  9. package/dist/gateway-C_Ae1HYB.d.mts +57 -0
  10. package/dist/gateway-C_Ae1HYB.d.mts.map +1 -0
  11. package/dist/index.d.mts +2 -0
  12. package/dist/index.mjs +2 -0
  13. package/dist/internal/events.d.mts +2 -0
  14. package/dist/internal/events.mjs +109 -0
  15. package/dist/internal/events.mjs.map +1 -0
  16. package/dist/internal/events.test.d.mts +1 -0
  17. package/dist/internal/events.test.mjs +130 -0
  18. package/dist/internal/events.test.mjs.map +1 -0
  19. package/dist/internal/gateway.d.mts +2 -0
  20. package/dist/internal/gateway.mjs +216 -0
  21. package/dist/internal/gateway.mjs.map +1 -0
  22. package/dist/internal/gateway.test.d.mts +1 -0
  23. package/dist/internal/gateway.test.mjs +40 -0
  24. package/dist/internal/gateway.test.mjs.map +1 -0
  25. package/dist/internal/rest.d.mts +2 -0
  26. package/dist/internal/rest.mjs +89 -0
  27. package/dist/internal/rest.mjs.map +1 -0
  28. package/dist/magic-string.es-CUAnjKYC.mjs +1014 -0
  29. package/dist/magic-string.es-CUAnjKYC.mjs.map +1 -0
  30. package/dist/rest-CZSW-VJK.d.mts +44 -0
  31. package/dist/rest-CZSW-VJK.d.mts.map +1 -0
  32. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  33. package/dist/test.DNmyFkvJ-N3QqMRwZ.mjs +13658 -0
  34. package/dist/test.DNmyFkvJ-N3QqMRwZ.mjs.map +1 -0
  35. package/package.json +59 -0
  36. package/src/Discord.ts +393 -0
  37. package/src/index.ts +1 -0
  38. package/src/internal/events.test.ts +89 -0
  39. package/src/internal/events.ts +197 -0
  40. package/src/internal/gateway.test.ts +27 -0
  41. package/src/internal/gateway.ts +368 -0
  42. package/src/internal/rest.ts +164 -0
@@ -0,0 +1,164 @@
1
+ import { Data, Duration, Effect, Redacted, Schema } from "effect"
2
+ import { HttpClient, HttpClientRequest } from "effect/unstable/http"
3
+ import * as MessengerError from "@effect-uai/core/MessengerError"
4
+ import * as Multipart from "@effect-uai/core/Multipart"
5
+
6
+ export const provider = "discord"
7
+
8
+ /** Tracks the package version; Discord requires a `DiscordBot (url, version)` agent. */
9
+ const userAgent = "DiscordBot (https://effect-uai.betalyra.com, 0.14.0)"
10
+
11
+ export type Config = {
12
+ readonly token: Redacted.Redacted
13
+ /** Defaults to `https://discord.com/api/v10`. Always version-pinned. */
14
+ readonly baseUrl?: string
15
+ }
16
+
17
+ export type Method = "GET" | "POST" | "PATCH" | "PUT" | "DELETE"
18
+
19
+ /** JSON body fields of a request. */
20
+ export type Fields = Readonly<Record<string, unknown>>
21
+
22
+ /**
23
+ * A REST call that came back non-2xx or never came back at all. `code` is
24
+ * Discord's own JSON error code, `status` the HTTP one; each verb maps this
25
+ * onto its own `MessengerError`.
26
+ */
27
+ export class ApiFailure extends Data.TaggedError("DiscordApiFailure")<{
28
+ readonly route: string
29
+ readonly status?: number
30
+ readonly code?: number
31
+ readonly message: string
32
+ readonly raw: unknown
33
+ }> {}
34
+
35
+ export type ApiError = ApiFailure | MessengerError.MessengerRateLimited
36
+
37
+ const ErrorBody = Schema.Struct({
38
+ message: Schema.optional(Schema.String),
39
+ code: Schema.optional(Schema.Number),
40
+ retry_after: Schema.optional(Schema.Number),
41
+ })
42
+
43
+ /** The file part of a multipart message. Discord names it `files[n]`. */
44
+ export type Upload = {
45
+ readonly bytes: Uint8Array
46
+ readonly filename: string
47
+ readonly mimeType: string
48
+ }
49
+
50
+ const endpoint = (cfg: Config, path: string): string =>
51
+ `${cfg.baseUrl ?? "https://discord.com/api/v10"}${path}`
52
+
53
+ const constructors: Record<Method, (url: string) => HttpClientRequest.HttpClientRequest> = {
54
+ GET: HttpClientRequest.get,
55
+ POST: HttpClientRequest.post,
56
+ PATCH: HttpClientRequest.patch,
57
+ PUT: HttpClientRequest.put,
58
+ DELETE: HttpClientRequest.delete,
59
+ }
60
+
61
+ const request = (cfg: Config, method: Method, path: string) =>
62
+ constructors[method](endpoint(cfg, path)).pipe(
63
+ HttpClientRequest.setHeaders({
64
+ Authorization: `Bot ${Redacted.value(cfg.token)}`,
65
+ "User-Agent": userAgent,
66
+ }),
67
+ )
68
+
69
+ const transportFailure = (route: string) => (raw: unknown) =>
70
+ new ApiFailure({ route, message: "transport failure", raw })
71
+
72
+ const rejected = (route: string, status: number, raw: unknown): Effect.Effect<never, ApiError> =>
73
+ Effect.gen(function* () {
74
+ const body = yield* Schema.decodeUnknownEffect(ErrorBody)(raw).pipe(
75
+ Effect.orElseSucceed((): typeof ErrorBody.Type => ({})),
76
+ )
77
+ // Discord's `retry_after` is float seconds; only a 429 carries one.
78
+ return yield* status === 429 && body.retry_after !== undefined
79
+ ? new MessengerError.MessengerRateLimited({
80
+ provider,
81
+ retryAfter: Duration.seconds(body.retry_after),
82
+ raw,
83
+ })
84
+ : new ApiFailure({
85
+ route,
86
+ status,
87
+ ...(body.code !== undefined && { code: body.code }),
88
+ message: body.message ?? `HTTP ${status}`,
89
+ raw,
90
+ })
91
+ })
92
+
93
+ const send = (
94
+ route: string,
95
+ httpRequest: HttpClientRequest.HttpClientRequest,
96
+ ): Effect.Effect<unknown, ApiError, HttpClient.HttpClient> =>
97
+ Effect.gen(function* () {
98
+ const client = yield* HttpClient.HttpClient
99
+ const response = yield* client
100
+ .execute(httpRequest)
101
+ .pipe(Effect.mapError(transportFailure(route)))
102
+ // 204 is the success shape of `typing`, `react` and every delete: no body
103
+ // to read, and reading one would fail.
104
+ if (response.status === 204) return undefined
105
+ const body = yield* response.json.pipe(Effect.mapError(transportFailure(route)))
106
+ return response.status < 400 ? body : yield* rejected(route, response.status, body)
107
+ })
108
+
109
+ /** One JSON call against the API base. The response body comes back undecoded. */
110
+ export const call =
111
+ (cfg: Config) =>
112
+ (
113
+ method: Method,
114
+ path: string,
115
+ body?: unknown,
116
+ ): Effect.Effect<unknown, ApiError, HttpClient.HttpClient> =>
117
+ send(
118
+ `${method} ${path}`,
119
+ body === undefined
120
+ ? request(cfg, method, path)
121
+ : request(cfg, method, path).pipe(HttpClientRequest.bodyJsonUnsafe(body)),
122
+ )
123
+
124
+ // The `attachments` entry is what binds `files[0]` to the message; without it
125
+ // Discord can drop the part and reject the rest as an empty message.
126
+ const formData = (payload: Fields, file: Upload): FormData => {
127
+ const form = new FormData()
128
+ form.append(
129
+ "payload_json",
130
+ JSON.stringify({ ...payload, attachments: [{ id: 0, filename: file.filename }] }),
131
+ )
132
+ form.append(
133
+ "files[0]",
134
+ new File([file.bytes as Uint8Array<ArrayBuffer>], file.filename, { type: file.mimeType }),
135
+ )
136
+ return form
137
+ }
138
+
139
+ /** One multipart message: the JSON fields as `payload_json` plus a single file. */
140
+ export const upload =
141
+ (cfg: Config) =>
142
+ (
143
+ path: string,
144
+ payload: Fields,
145
+ file: Upload,
146
+ ): Effect.Effect<unknown, ApiError, HttpClient.HttpClient> =>
147
+ Effect.gen(function* () {
148
+ const route = `POST ${path}`
149
+ const body = yield* Multipart.bodyMultipart(formData(payload, file)).pipe(
150
+ Effect.mapError(transportFailure(route)),
151
+ )
152
+ return yield* send(route, request(cfg, "POST", path).pipe(body))
153
+ })
154
+
155
+ /** Decode a response body; a shape mismatch is a malformed response, not a defect. */
156
+ export const decoded =
157
+ <A>(route: string, schema: Schema.Decoder<A>) =>
158
+ <E, R>(result: Effect.Effect<unknown, E, R>): Effect.Effect<A, E | ApiFailure, R> =>
159
+ Effect.gen(function* () {
160
+ const raw = yield* result
161
+ return yield* Schema.decodeUnknownEffect(schema)(raw).pipe(
162
+ Effect.mapError(() => new ApiFailure({ route, message: "malformed response", raw })),
163
+ )
164
+ })