@f3liz/rescript-misskey-api 0.6.9 → 0.8.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 (113) hide show
  1. package/lib/es6/src/Misskey.mjs +472 -159
  2. package/lib/es6/src/bindings/Ofetch.mjs +2 -0
  3. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkAccount.mjs +70 -48
  4. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkAdmin.mjs +133 -98
  5. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkAntennas.mjs +9 -6
  6. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkApp.mjs +6 -4
  7. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkAuth.mjs +6 -4
  8. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkChannels.mjs +18 -12
  9. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkCharts.mjs +6 -4
  10. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkChat.mjs +70 -54
  11. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkClips.mjs +12 -8
  12. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkDefault.mjs +27 -20
  13. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkDrive.mjs +35 -24
  14. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkFederation.mjs +18 -12
  15. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkFlash.mjs +9 -6
  16. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkFlashs.mjs +3 -2
  17. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkFollowing.mjs +21 -14
  18. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkGallery.mjs +12 -8
  19. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkGroups.mjs +30 -24
  20. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkHashtags.mjs +6 -4
  21. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkLists.mjs +9 -6
  22. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkMeta.mjs +27 -18
  23. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkNotes.mjs +85 -58
  24. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkNotifications.mjs +2 -2
  25. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkPages.mjs +8 -6
  26. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkRole.mjs +9 -6
  27. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkUsers.mjs +35 -24
  28. package/lib/es6/src/generated/kokonect-link/api/KokonectLinkWebhooks.mjs +7 -6
  29. package/lib/es6/src/generated/misskey-io/api/MisskeyIoAccount.mjs +92 -68
  30. package/lib/es6/src/generated/misskey-io/api/MisskeyIoAdmin.mjs +266 -214
  31. package/lib/es6/src/generated/misskey-io/api/MisskeyIoAntennas.mjs +17 -12
  32. package/lib/es6/src/generated/misskey-io/api/MisskeyIoApp.mjs +6 -4
  33. package/lib/es6/src/generated/misskey-io/api/MisskeyIoAuth.mjs +14 -10
  34. package/lib/es6/src/generated/misskey-io/api/MisskeyIoChannels.mjs +35 -26
  35. package/lib/es6/src/generated/misskey-io/api/MisskeyIoCharts.mjs +72 -48
  36. package/lib/es6/src/generated/misskey-io/api/MisskeyIoClip.mjs +4 -4
  37. package/lib/es6/src/generated/misskey-io/api/MisskeyIoClips.mjs +17 -12
  38. package/lib/es6/src/generated/misskey-io/api/MisskeyIoDefault.mjs +133 -110
  39. package/lib/es6/src/generated/misskey-io/api/MisskeyIoDrive.mjs +51 -36
  40. package/lib/es6/src/generated/misskey-io/api/MisskeyIoFederation.mjs +32 -22
  41. package/lib/es6/src/generated/misskey-io/api/MisskeyIoFlash.mjs +12 -10
  42. package/lib/es6/src/generated/misskey-io/api/MisskeyIoFlashs.mjs +5 -4
  43. package/lib/es6/src/generated/misskey-io/api/MisskeyIoFollowing.mjs +27 -20
  44. package/lib/es6/src/generated/misskey-io/api/MisskeyIoGallery.mjs +24 -18
  45. package/lib/es6/src/generated/misskey-io/api/MisskeyIoHashtags.mjs +18 -12
  46. package/lib/es6/src/generated/misskey-io/api/MisskeyIoLists.mjs +23 -18
  47. package/lib/es6/src/generated/misskey-io/api/MisskeyIoMeta.mjs +71 -48
  48. package/lib/es6/src/generated/misskey-io/api/MisskeyIoNonProductive.mjs +5 -4
  49. package/lib/es6/src/generated/misskey-io/api/MisskeyIoNotes.mjs +96 -70
  50. package/lib/es6/src/generated/misskey-io/api/MisskeyIoNotifications.mjs +8 -8
  51. package/lib/es6/src/generated/misskey-io/api/MisskeyIoPages.mjs +17 -14
  52. package/lib/es6/src/generated/misskey-io/api/MisskeyIoReactions.mjs +4 -4
  53. package/lib/es6/src/generated/misskey-io/api/MisskeyIoResetPassword.mjs +4 -4
  54. package/lib/es6/src/generated/misskey-io/api/MisskeyIoRole.mjs +12 -8
  55. package/lib/es6/src/generated/misskey-io/api/MisskeyIoUsers.mjs +83 -56
  56. package/lib/es6/src/generated/misskey-io/api/MisskeyIoWebhooks.mjs +17 -14
  57. package/package.json +3 -2
  58. package/src/Misskey.res +423 -151
  59. package/src/bindings/Ofetch.res +14 -0
  60. package/src/generated/kokonect-link/api/KokonectLinkAccount.res +94 -166
  61. package/src/generated/kokonect-link/api/KokonectLinkAdmin.res +182 -329
  62. package/src/generated/kokonect-link/api/KokonectLinkAntennas.res +12 -21
  63. package/src/generated/kokonect-link/api/KokonectLinkApp.res +8 -14
  64. package/src/generated/kokonect-link/api/KokonectLinkAuth.res +8 -14
  65. package/src/generated/kokonect-link/api/KokonectLinkChannels.res +24 -42
  66. package/src/generated/kokonect-link/api/KokonectLinkCharts.res +8 -14
  67. package/src/generated/kokonect-link/api/KokonectLinkChat.res +97 -178
  68. package/src/generated/kokonect-link/api/KokonectLinkClips.res +16 -28
  69. package/src/generated/kokonect-link/api/KokonectLinkDefault.res +37 -67
  70. package/src/generated/kokonect-link/api/KokonectLinkDrive.res +47 -83
  71. package/src/generated/kokonect-link/api/KokonectLinkFederation.res +24 -42
  72. package/src/generated/kokonect-link/api/KokonectLinkFlash.res +12 -21
  73. package/src/generated/kokonect-link/api/KokonectLinkFlashs.res +4 -7
  74. package/src/generated/kokonect-link/api/KokonectLinkFollowing.res +28 -49
  75. package/src/generated/kokonect-link/api/KokonectLinkGallery.res +16 -28
  76. package/src/generated/kokonect-link/api/KokonectLinkGroups.res +42 -78
  77. package/src/generated/kokonect-link/api/KokonectLinkHashtags.res +8 -14
  78. package/src/generated/kokonect-link/api/KokonectLinkLists.res +12 -21
  79. package/src/generated/kokonect-link/api/KokonectLinkMeta.res +36 -63
  80. package/src/generated/kokonect-link/api/KokonectLinkNotes.res +114 -201
  81. package/src/generated/kokonect-link/api/KokonectLinkNotifications.res +3 -6
  82. package/src/generated/kokonect-link/api/KokonectLinkPages.res +11 -20
  83. package/src/generated/kokonect-link/api/KokonectLinkRole.res +12 -21
  84. package/src/generated/kokonect-link/api/KokonectLinkUsers.res +47 -83
  85. package/src/generated/kokonect-link/api/KokonectLinkWebhooks.res +10 -19
  86. package/src/generated/misskey-io/api/MisskeyIoAccount.res +126 -228
  87. package/src/generated/misskey-io/api/MisskeyIoAdmin.res +373 -694
  88. package/src/generated/misskey-io/api/MisskeyIoAntennas.res +23 -41
  89. package/src/generated/misskey-io/api/MisskeyIoApp.res +8 -14
  90. package/src/generated/misskey-io/api/MisskeyIoAuth.res +19 -34
  91. package/src/generated/misskey-io/api/MisskeyIoChannels.res +48 -87
  92. package/src/generated/misskey-io/api/MisskeyIoCharts.res +96 -168
  93. package/src/generated/misskey-io/api/MisskeyIoClip.res +6 -12
  94. package/src/generated/misskey-io/api/MisskeyIoClips.res +23 -41
  95. package/src/generated/misskey-io/api/MisskeyIoDefault.res +188 -353
  96. package/src/generated/misskey-io/api/MisskeyIoDrive.res +69 -123
  97. package/src/generated/misskey-io/api/MisskeyIoFederation.res +43 -76
  98. package/src/generated/misskey-io/api/MisskeyIoFlash.res +17 -32
  99. package/src/generated/misskey-io/api/MisskeyIoFlashs.res +7 -13
  100. package/src/generated/misskey-io/api/MisskeyIoFollowing.res +37 -67
  101. package/src/generated/misskey-io/api/MisskeyIoGallery.res +33 -60
  102. package/src/generated/misskey-io/api/MisskeyIoHashtags.res +24 -42
  103. package/src/generated/misskey-io/api/MisskeyIoLists.res +32 -59
  104. package/src/generated/misskey-io/api/MisskeyIoMeta.res +95 -167
  105. package/src/generated/misskey-io/api/MisskeyIoNonProductive.res +7 -13
  106. package/src/generated/misskey-io/api/MisskeyIoNotes.res +131 -236
  107. package/src/generated/misskey-io/api/MisskeyIoNotifications.res +12 -24
  108. package/src/generated/misskey-io/api/MisskeyIoPages.res +24 -45
  109. package/src/generated/misskey-io/api/MisskeyIoReactions.res +6 -12
  110. package/src/generated/misskey-io/api/MisskeyIoResetPassword.res +6 -12
  111. package/src/generated/misskey-io/api/MisskeyIoRole.res +16 -28
  112. package/src/generated/misskey-io/api/MisskeyIoUsers.res +111 -195
  113. package/src/generated/misskey-io/api/MisskeyIoWebhooks.res +24 -45
package/src/Misskey.res CHANGED
@@ -18,30 +18,9 @@
18
18
  // You can override it by passing ~fetch to connect() for custom behavior
19
19
  // (e.g., adding performance metrics, retries, logging, etc.).
20
20
 
21
- // ============================================================================
22
- // Fetch Bindings
23
- // ============================================================================
24
-
25
21
  // Enable JSON schema for Sury
26
22
  S.enableJson()
27
23
 
28
- module FetchBindings = {
29
- type response
30
-
31
- type requestInit = {
32
- method: string,
33
- headers: dict<string>,
34
- body: option<string>,
35
- }
36
-
37
- @val external fetch: (string, requestInit) => promise<response> = "fetch"
38
-
39
- @send external json: response => promise<JSON.t> = "json"
40
- @get external ok: response => bool = "ok"
41
- @get external status: response => int = "status"
42
- @get external statusText: response => string = "statusText"
43
- }
44
-
45
24
  // ============================================================================
46
25
  // Client & Configuration
47
26
  // ============================================================================
@@ -56,50 +35,34 @@ type t = {
56
35
  mutable streamClient: option<StreamClient.t>,
57
36
  }
58
37
 
59
- // Default fetch implementation using the browser's Fetch API.
60
- // Constructs full URL from origin + endpoint path, sets JSON headers,
61
- // and injects the auth token into the request body (Misskey API convention).
62
- let defaultFetch = (~origin: string, ~token: option<string>) => {
63
- (~url: string, ~method_: string, ~body: option<JSON.t>): Promise.t<JSON.t> => {
64
- let fullUrl = `${origin}/api/${url}`
65
-
66
- // Misskey injects the token into the request body
67
- let bodyWithToken = switch (body, token) {
68
- | (Some(jsonBody), Some(t)) =>
69
- switch jsonBody->JSON.Decode.object {
70
- | Some(obj) =>
71
- obj->Dict.set("i", t->JSON.Encode.string)
72
- Some(obj->JSON.Encode.object)
73
- | None => body
74
- }
75
- | (None, Some(t)) =>
76
- let obj = Dict.make()
38
+ // Inject auth token into request body (Misskey API convention)
39
+ let injectToken = (body: option<JSON.t>, token: option<string>): option<JSON.t> => {
40
+ switch (body, token) {
41
+ | (Some(jsonBody), Some(t)) =>
42
+ switch jsonBody->JSON.Decode.object {
43
+ | Some(obj) =>
77
44
  obj->Dict.set("i", t->JSON.Encode.string)
78
45
  Some(obj->JSON.Encode.object)
79
- | _ => body
46
+ | None => body
80
47
  }
48
+ | (None, Some(t)) =>
49
+ let obj = Dict.make()
50
+ obj->Dict.set("i", t->JSON.Encode.string)
51
+ Some(obj->JSON.Encode.object)
52
+ | _ => body
53
+ }
54
+ }
81
55
 
82
- let bodyStr = bodyWithToken->Option.map(json => JSON.stringify(json))
83
-
84
- let headers = Dict.make()
85
- headers->Dict.set("Content-Type", "application/json")
86
-
87
- FetchBindings.fetch(
88
- fullUrl,
89
- {
90
- method: method_,
91
- headers,
92
- body: bodyStr,
93
- },
94
- )->Promise.then(response => {
95
- if FetchBindings.ok(response) {
96
- response->FetchBindings.json
97
- } else {
98
- let msg = `API error: ${FetchBindings.status(
99
- response,
100
- )->Int.toString} ${FetchBindings.statusText(response)}`
101
- Promise.reject(JsExn(JsError.make(msg)->Obj.magic))
102
- }
56
+ // Default fetch implementation using ofetch.
57
+ // Uses baseURL for origin, auto JSON parsing, and token injection.
58
+ let defaultFetch = (~origin: string, ~token: option<string>) => {
59
+ (~url: string, ~method_: string, ~body: option<JSON.t>): Promise.t<JSON.t> => {
60
+ let bodyWithToken = injectToken(body, token)
61
+ Ofetch.ofetch(url, {
62
+ baseURL: `${origin}/api`,
63
+ method: method_,
64
+ body: ?bodyWithToken,
65
+ retry: 0,
103
66
  })
104
67
  }
105
68
  }
@@ -143,21 +106,42 @@ let wrapperConnect = (client: t): MisskeyIoWrapper.client => {
143
106
  ///
144
107
  /// Example:
145
108
  /// let user = await client->Misskey.request("i", ())
146
- let request = (
109
+ let request = async (
147
110
  client: t,
148
111
  endpoint: string,
149
112
  ~params: JSON.t=JSON.Encode.object(Dict.make()),
150
113
  (),
151
- ): promise<result<JSON.t, string>> => {
152
- client.fetchFn(~url=endpoint, ~method_="POST", ~body=Some(params))
153
- ->Promise.then(json => Ok(json)->Promise.resolve)
154
- ->Promise.catch(err => {
155
- let msg = switch err->JsExn.fromException {
114
+ ): result<JSON.t, string> => {
115
+ try {
116
+ let json = await client.fetchFn(~url=endpoint, ~method_="POST", ~body=Some(params))
117
+ Ok(json)
118
+ } catch {
119
+ | err =>
120
+ // ofetch attaches response data to error.data for non-2xx responses
121
+ let errorData: option<JSON.t> = (err->Obj.magic)["data"]
122
+ let baseMsg = switch err->JsExn.fromException {
156
123
  | Some(jsExn) => JsExn.message(jsExn)->Option.getOr("Unknown error")
157
124
  | None => "Unknown error"
158
125
  }
159
- Error(msg)->Promise.resolve
160
- })
126
+ let msg = switch errorData {
127
+ | Some(data) =>
128
+ switch data->JSON.Decode.object {
129
+ | Some(obj) =>
130
+ switch obj->Dict.get("error")->Option.flatMap(JSON.Decode.object) {
131
+ | Some(errObj) =>
132
+ let code =
133
+ errObj->Dict.get("code")->Option.flatMap(JSON.Decode.string)->Option.getOr("")
134
+ let message =
135
+ errObj->Dict.get("message")->Option.flatMap(JSON.Decode.string)->Option.getOr("")
136
+ `${baseMsg} [${code}] ${message}`
137
+ | None => baseMsg
138
+ }
139
+ | None => baseMsg
140
+ }
141
+ | None => baseMsg
142
+ }
143
+ Error(msg)
144
+ }
161
145
  }
162
146
 
163
147
  /// Get current user info.
@@ -168,6 +152,9 @@ let currentUser = (client: t): promise<result<JSON.t, string>> => {
168
152
  /// Get client origin (instance URL).
169
153
  let origin = (client: t): string => client.origin
170
154
 
155
+ /// Get client authentication token.
156
+ let token = (client: t): option<string> => client.token
157
+
171
158
  /// Close client and cleanup (close streaming connections).
172
159
  let close = (client: t): unit => {
173
160
  client.streamClient->Option.forEach(s => s->StreamClient.close)
@@ -200,6 +187,7 @@ module Notes = {
200
187
  ~localOnly: bool=false,
201
188
  ~replyId: option<string>=?,
202
189
  ~renoteId: option<string>=?,
190
+ ~fileIds: option<array<string>>=?,
203
191
  (),
204
192
  ): promise<result<JSON.t, string>> => {
205
193
  let params = Dict.make()
@@ -217,6 +205,9 @@ module Notes = {
217
205
  cw->Option.forEach(v => params->Dict.set("cw", v->JSON.Encode.string))
218
206
  replyId->Option.forEach(v => params->Dict.set("replyId", v->JSON.Encode.string))
219
207
  renoteId->Option.forEach(v => params->Dict.set("renoteId", v->JSON.Encode.string))
208
+ fileIds->Option.forEach(ids =>
209
+ params->Dict.set("fileIds", ids->Array.map(JSON.Encode.string)->JSON.Encode.array)
210
+ )
220
211
 
221
212
  request(client, "notes/create", ~params=params->JSON.Encode.object, ())
222
213
  }
@@ -292,12 +283,89 @@ module Notes = {
292
283
  params->Dict.set("noteId", noteId->JSON.Encode.string)
293
284
  request(client, "notes/reactions/delete", ~params=params->JSON.Encode.object, ())
294
285
  }
286
+
287
+ /// Get a single note by ID.
288
+ let show = (client: t, noteId: string): promise<result<JSON.t, string>> => {
289
+ let params = Dict.make()
290
+ params->Dict.set("noteId", noteId->JSON.Encode.string)
291
+ request(client, "notes/show", ~params=params->JSON.Encode.object, ())
292
+ }
293
+
294
+ /// Get replies/children of a note.
295
+ let children = (
296
+ client: t,
297
+ noteId: string,
298
+ ~limit: int=30,
299
+ ~sinceId: option<string>=?,
300
+ ~untilId: option<string>=?,
301
+ (),
302
+ ): promise<result<JSON.t, string>> => {
303
+ let params = Dict.make()
304
+ params->Dict.set("noteId", noteId->JSON.Encode.string)
305
+ params->Dict.set("limit", limit->JSON.Encode.int)
306
+ sinceId->Option.forEach(v => params->Dict.set("sinceId", v->JSON.Encode.string))
307
+ untilId->Option.forEach(v => params->Dict.set("untilId", v->JSON.Encode.string))
308
+ request(client, "notes/children", ~params=params->JSON.Encode.object, ())
309
+ }
310
+
311
+ /// Get the conversation thread (parent notes) for a note.
312
+ let conversation = (
313
+ client: t,
314
+ noteId: string,
315
+ ~limit: int=30,
316
+ (),
317
+ ): promise<result<JSON.t, string>> => {
318
+ let params = Dict.make()
319
+ params->Dict.set("noteId", noteId->JSON.Encode.string)
320
+ params->Dict.set("limit", limit->JSON.Encode.int)
321
+ request(client, "notes/conversation", ~params=params->JSON.Encode.object, ())
322
+ }
295
323
  }
296
324
 
297
325
  // ============================================================================
298
- // Stream API - Real-time updates via WebSocket
326
+ // Users API
299
327
  // ============================================================================
300
328
 
329
+ module Users = {
330
+ /// Get user profile by username (and optional host for remote users).
331
+ let show = (
332
+ client: t,
333
+ ~userId: option<string>=?,
334
+ ~username: option<string>=?,
335
+ ~host: option<string>=?,
336
+ (),
337
+ ): promise<result<JSON.t, string>> => {
338
+ let params = Dict.make()
339
+ userId->Option.forEach(v => params->Dict.set("userId", v->JSON.Encode.string))
340
+ username->Option.forEach(v => params->Dict.set("username", v->JSON.Encode.string))
341
+ host->Option.forEach(v => params->Dict.set("host", v->JSON.Encode.string))
342
+ request(client, "users/show", ~params=params->JSON.Encode.object, ())
343
+ }
344
+
345
+ /// Get notes posted by a user.
346
+ let notes = (
347
+ client: t,
348
+ userId: string,
349
+ ~limit: int=20,
350
+ ~withReplies: bool=false,
351
+ ~withRenotes: bool=true,
352
+ ~withFiles: bool=false,
353
+ ~sinceId: option<string>=?,
354
+ ~untilId: option<string>=?,
355
+ (),
356
+ ): promise<result<JSON.t, string>> => {
357
+ let params = Dict.make()
358
+ params->Dict.set("userId", userId->JSON.Encode.string)
359
+ params->Dict.set("limit", limit->JSON.Encode.int)
360
+ params->Dict.set("withReplies", withReplies->JSON.Encode.bool)
361
+ params->Dict.set("withRenotes", withRenotes->JSON.Encode.bool)
362
+ params->Dict.set("withFiles", withFiles->JSON.Encode.bool)
363
+ sinceId->Option.forEach(v => params->Dict.set("sinceId", v->JSON.Encode.string))
364
+ untilId->Option.forEach(v => params->Dict.set("untilId", v->JSON.Encode.string))
365
+ request(client, "users/notes", ~params=params->JSON.Encode.object, ())
366
+ }
367
+ }
368
+
301
369
  module Stream = {
302
370
  type subscription = {dispose: unit => unit}
303
371
 
@@ -413,21 +481,20 @@ module Emojis = {
413
481
  }
414
482
 
415
483
  /// Get list of custom emojis from instance.
416
- let list = (client: t): promise<result<array<customEmoji>, string>> => {
417
- request(client, "emojis", ())->Promise.then(result => {
418
- switch result {
419
- | Ok(json) =>
420
- switch json->JSON.Decode.object {
421
- | Some(obj) =>
422
- switch obj->Dict.get("emojis")->Option.flatMap(JSON.Decode.array) {
423
- | Some(emojisArray) => Ok(emojisArray->Array.filterMap(decodeCustomEmoji))
424
- | None => Ok([])
425
- }
484
+ let list = async (client: t): result<array<customEmoji>, string> => {
485
+ let result = await request(client, "emojis", ())
486
+ switch result {
487
+ | Ok(json) =>
488
+ switch json->JSON.Decode.object {
489
+ | Some(obj) =>
490
+ switch obj->Dict.get("emojis")->Option.flatMap(JSON.Decode.array) {
491
+ | Some(emojisArray) => Ok(emojisArray->Array.filterMap(decodeCustomEmoji))
426
492
  | None => Ok([])
427
493
  }
428
- | Error(e) => Error(e)
429
- }->Promise.resolve
430
- })
494
+ | None => Ok([])
495
+ }
496
+ | Error(e) => Error(e)
497
+ }
431
498
  }
432
499
  }
433
500
 
@@ -437,45 +504,42 @@ module Emojis = {
437
504
 
438
505
  module CustomTimelines = {
439
506
  /// Fetch user's antennas.
440
- let antennas = (client: t): promise<result<array<JSON.t>, string>> => {
441
- request(client, "antennas/list", ())->Promise.then(result => {
442
- switch result {
443
- | Ok(json) =>
444
- switch json->JSON.Decode.array {
445
- | Some(arr) => Ok(arr)
446
- | None => Ok([])
447
- }
448
- | Error(e) => Error(e)
449
- }->Promise.resolve
450
- })
507
+ let antennas = async (client: t): result<array<JSON.t>, string> => {
508
+ let result = await request(client, "antennas/list", ())
509
+ switch result {
510
+ | Ok(json) =>
511
+ switch json->JSON.Decode.array {
512
+ | Some(arr) => Ok(arr)
513
+ | None => Ok([])
514
+ }
515
+ | Error(e) => Error(e)
516
+ }
451
517
  }
452
518
 
453
519
  /// Fetch user's lists.
454
- let lists = (client: t): promise<result<array<JSON.t>, string>> => {
455
- request(client, "users/lists/list", ())->Promise.then(result => {
456
- switch result {
457
- | Ok(json) =>
458
- switch json->JSON.Decode.array {
459
- | Some(arr) => Ok(arr)
460
- | None => Ok([])
461
- }
462
- | Error(e) => Error(e)
463
- }->Promise.resolve
464
- })
520
+ let lists = async (client: t): result<array<JSON.t>, string> => {
521
+ let result = await request(client, "users/lists/list", ())
522
+ switch result {
523
+ | Ok(json) =>
524
+ switch json->JSON.Decode.array {
525
+ | Some(arr) => Ok(arr)
526
+ | None => Ok([])
527
+ }
528
+ | Error(e) => Error(e)
529
+ }
465
530
  }
466
531
 
467
532
  /// Fetch user's followed channels.
468
- let channels = (client: t): promise<result<array<JSON.t>, string>> => {
469
- request(client, "channels/followed", ())->Promise.then(result => {
470
- switch result {
471
- | Ok(json) =>
472
- switch json->JSON.Decode.array {
473
- | Some(arr) => Ok(arr)
474
- | None => Ok([])
475
- }
476
- | Error(e) => Error(e)
477
- }->Promise.resolve
478
- })
533
+ let channels = async (client: t): result<array<JSON.t>, string> => {
534
+ let result = await request(client, "channels/followed", ())
535
+ switch result {
536
+ | Ok(json) =>
537
+ switch json->JSON.Decode.array {
538
+ | Some(arr) => Ok(arr)
539
+ | None => Ok([])
540
+ }
541
+ | Error(e) => Error(e)
542
+ }
479
543
  }
480
544
 
481
545
  /// Extract ID and name from a timeline item JSON.
@@ -589,15 +653,10 @@ module MiAuth = {
589
653
  }
590
654
 
591
655
  @val external encodeURIComponent: string => string = "encodeURIComponent"
656
+ @val @scope("crypto") external randomUUID: unit => string = "randomUUID"
592
657
 
593
658
  let generateSessionId = (): string => {
594
- %raw(`
595
- function() {
596
- var a = new Uint8Array(16);
597
- crypto.getRandomValues(a);
598
- return Array.from(a, function(b) { return b.toString(16).padStart(2, '0'); }).join('');
599
- }()
600
- `)
659
+ randomUUID()->String.replaceAll("-", "")
601
660
  }
602
661
 
603
662
  /// Generate MiAuth URL for user authorization.
@@ -637,35 +696,22 @@ module MiAuth = {
637
696
  /// authorization is complete, and `{ok: false}` when still pending.
638
697
  let check = async (~origin: string, ~sessionId: string): result<checkResult, string> => {
639
698
  try {
640
- let url = `${origin}/api/miauth/${sessionId}/check`
641
-
642
- let headers = Dict.make()
643
- headers->Dict.set("Content-Type", "application/json")
644
-
645
- let response = await FetchBindings.fetch(url, {method: "POST", headers, body: Some("{}")})
646
-
647
- if !FetchBindings.ok(response) {
648
- Error(
649
- `HTTP error: ${FetchBindings.status(response)->Int.toString} ${FetchBindings.statusText(
650
- response,
651
- )}`,
652
- )
653
- } else {
654
- let json = await response->FetchBindings.json
655
- switch json->JSON.Decode.object {
656
- | Some(obj) =>
657
- // Check the "ok" field in the response body
658
- let isOk = obj->Dict.get("ok")->Option.flatMap(JSON.Decode.bool)->Option.getOr(false)
659
- if isOk {
660
- let token = obj->Dict.get("token")->Option.flatMap(JSON.Decode.string)
661
- let user = obj->Dict.get("user")
662
- Ok({token, user})
663
- } else {
664
- // Server explicitly says not authorized yet
665
- Ok({token: None, user: None})
666
- }
667
- | None => Error("Unexpected response format")
699
+ let json = await Ofetch.ofetch(`${origin}/api/miauth/${sessionId}/check`, {
700
+ method: "POST",
701
+ body: JSON.Encode.object(Dict.make()),
702
+ retry: 0,
703
+ })
704
+ switch json->JSON.Decode.object {
705
+ | Some(obj) =>
706
+ let isOk = obj->Dict.get("ok")->Option.flatMap(JSON.Decode.bool)->Option.getOr(false)
707
+ if isOk {
708
+ let token = obj->Dict.get("token")->Option.flatMap(JSON.Decode.string)
709
+ let user = obj->Dict.get("user")
710
+ Ok({token, user})
711
+ } else {
712
+ Ok({token: None, user: None})
668
713
  }
714
+ | None => Error("Unexpected response format")
669
715
  }
670
716
  } catch {
671
717
  | err =>
@@ -717,6 +763,166 @@ let isPermissionDenied = (error: JSON.t): bool => {
717
763
  }
718
764
 
719
765
  /// Check if error is an API error and extract error info.
766
+ // ============================================================================
767
+ // Instance Meta API
768
+ // ============================================================================
769
+
770
+ module Meta = {
771
+ type meta = {
772
+ swPublickey: option<string>,
773
+ }
774
+
775
+ /// Get instance metadata (includes VAPID public key for push notifications).
776
+ let get = async (client: t): result<meta, string> => {
777
+ switch await request(client, "meta", ()) {
778
+ | Ok(json) =>
779
+ switch json->JSON.Decode.object {
780
+ | Some(obj) =>
781
+ Ok({
782
+ swPublickey: obj
783
+ ->Dict.get("swPublickey")
784
+ ->Option.flatMap(JSON.Decode.string),
785
+ })
786
+ | None => Error("Invalid meta response")
787
+ }
788
+ | Error(e) => Error(e)
789
+ }
790
+ }
791
+ }
792
+
793
+ // ============================================================================
794
+ // Service Worker (Push Notification) API
795
+ // ============================================================================
796
+
797
+ module Sw = {
798
+ type registration = {
799
+ state: option<string>,
800
+ key: option<string>,
801
+ userId: string,
802
+ endpoint: string,
803
+ sendReadMessage: bool,
804
+ }
805
+
806
+ /// Register a push notification endpoint with the Misskey instance.
807
+ let register = async (
808
+ client: t,
809
+ ~endpoint: string,
810
+ ~auth: string,
811
+ ~publickey: string,
812
+ ~sendReadMessage: bool=false,
813
+ (),
814
+ ): result<registration, string> => {
815
+ let params = Dict.make()
816
+ params->Dict.set("endpoint", endpoint->JSON.Encode.string)
817
+ params->Dict.set("auth", auth->JSON.Encode.string)
818
+ params->Dict.set("publickey", publickey->JSON.Encode.string)
819
+ params->Dict.set("sendReadMessage", sendReadMessage->JSON.Encode.bool)
820
+ switch await request(client, "sw/register", ~params=params->JSON.Encode.object, ()) {
821
+ | Ok(json) =>
822
+ switch json->JSON.Decode.object {
823
+ | Some(obj) =>
824
+ Ok({
825
+ state: obj->Dict.get("state")->Option.flatMap(JSON.Decode.string),
826
+ key: obj->Dict.get("key")->Option.flatMap(JSON.Decode.string),
827
+ userId: obj->Dict.get("userId")->Option.flatMap(JSON.Decode.string)->Option.getOr(""),
828
+ endpoint: obj->Dict.get("endpoint")->Option.flatMap(JSON.Decode.string)->Option.getOr(""),
829
+ sendReadMessage: obj
830
+ ->Dict.get("sendReadMessage")
831
+ ->Option.flatMap(JSON.Decode.bool)
832
+ ->Option.getOr(false),
833
+ })
834
+ | None => Error("Invalid sw/register response")
835
+ }
836
+ | Error(e) => Error(e)
837
+ }
838
+ }
839
+
840
+ /// Unregister a push notification endpoint.
841
+ let unregister = async (client: t, ~endpoint: string): result<unit, string> => {
842
+ let params = Dict.make()
843
+ params->Dict.set("endpoint", endpoint->JSON.Encode.string)
844
+ switch await request(client, "sw/unregister", ~params=params->JSON.Encode.object, ()) {
845
+ | Ok(_) => Ok()
846
+ | Error(e) => Error(e)
847
+ }
848
+ }
849
+ }
850
+
851
+ // ============================================================================
852
+ // Webhooks API
853
+ // ============================================================================
854
+
855
+ module Webhooks = {
856
+ type webhook = {
857
+ id: string,
858
+ name: string,
859
+ url: string,
860
+ active: bool,
861
+ }
862
+
863
+ /// Create a webhook on the Misskey instance.
864
+ let create = async (
865
+ client: t,
866
+ ~name: string,
867
+ ~url: string,
868
+ ~secret: string,
869
+ ~on: array<string>,
870
+ (),
871
+ ): result<webhook, string> => {
872
+ let params = Dict.make()
873
+ params->Dict.set("name", name->JSON.Encode.string)
874
+ params->Dict.set("url", url->JSON.Encode.string)
875
+ params->Dict.set("secret", secret->JSON.Encode.string)
876
+ params->Dict.set("on", on->Array.map(JSON.Encode.string)->JSON.Encode.array)
877
+ switch await request(client, "i/webhooks/create", ~params=params->JSON.Encode.object, ()) {
878
+ | Ok(json) =>
879
+ switch json->JSON.Decode.object {
880
+ | Some(obj) =>
881
+ let id = obj->Dict.get("id")->Option.flatMap(JSON.Decode.string)->Option.getOr("")
882
+ let name = obj->Dict.get("name")->Option.flatMap(JSON.Decode.string)->Option.getOr("")
883
+ let url = obj->Dict.get("url")->Option.flatMap(JSON.Decode.string)->Option.getOr("")
884
+ let active = obj->Dict.get("active")->Option.flatMap(JSON.Decode.bool)->Option.getOr(false)
885
+ Ok({id, name, url, active})
886
+ | None => Error("Invalid webhook response")
887
+ }
888
+ | Error(e) => Error(e)
889
+ }
890
+ }
891
+
892
+ /// Delete a webhook by ID.
893
+ let delete = async (client: t, ~webhookId: string): result<unit, string> => {
894
+ let params = Dict.make()
895
+ params->Dict.set("webhookId", webhookId->JSON.Encode.string)
896
+ switch await request(client, "i/webhooks/delete", ~params=params->JSON.Encode.object, ()) {
897
+ | Ok(_) => Ok()
898
+ | Error(e) => Error(e)
899
+ }
900
+ }
901
+
902
+ /// List webhooks for the current user.
903
+ let list = async (client: t): result<array<webhook>, string> => {
904
+ switch await request(client, "i/webhooks/list", ()) {
905
+ | Ok(json) =>
906
+ switch json->JSON.Decode.array {
907
+ | Some(arr) =>
908
+ Ok(arr->Array.filterMap(item => {
909
+ switch item->JSON.Decode.object {
910
+ | Some(obj) =>
911
+ let id = obj->Dict.get("id")->Option.flatMap(JSON.Decode.string)->Option.getOr("")
912
+ let name = obj->Dict.get("name")->Option.flatMap(JSON.Decode.string)->Option.getOr("")
913
+ let url = obj->Dict.get("url")->Option.flatMap(JSON.Decode.string)->Option.getOr("")
914
+ let active = obj->Dict.get("active")->Option.flatMap(JSON.Decode.bool)->Option.getOr(false)
915
+ Some({id, name, url, active})
916
+ | None => None
917
+ }
918
+ }))
919
+ | None => Ok([])
920
+ }
921
+ | Error(e) => Error(e)
922
+ }
923
+ }
924
+ }
925
+
720
926
  let isAPIError = (error: JSON.t): option<apiError> => {
721
927
  switch error->JSON.Decode.object {
722
928
  | Some(obj) =>
@@ -732,4 +938,70 @@ let isAPIError = (error: JSON.t): option<apiError> => {
732
938
  }
733
939
  }
734
940
 
941
+ // ============================================================================
942
+ // Drive API
943
+ // ============================================================================
944
+ // NOTE: All HTTP calls in this module (and throughout Misskey.res) should use
945
+ // `Ofetch.ofetch` rather than raw `fetch`. ofetch handles JSON parsing,
946
+ // non-2xx error throwing, and runtime FormData detection automatically, which
947
+ // avoids the pitfall of calling `response.json()` on non-standard objects.
948
+
949
+ module Drive = {
950
+ /// Upload a File object to the Misskey drive.
951
+ /// Returns the drive file ID on success.
952
+ let upload = async (client: t, ~file: {..}, ~sensitive: bool=false, ()): result<string, string> => {
953
+ try {
954
+ let fd: {..} = %raw(`new FormData()`)
955
+ fd["append"]("file", file)
956
+ client.token->Option.forEach(tok => fd["append"]("i", tok))
957
+ if sensitive { fd["append"]("isSensitive", "true") }
958
+
959
+ let endpoint = client.origin ++ "/api/drive/files/create"
960
+ // Cast FormData to JSON.t so ofetch can accept it;
961
+ // ofetch detects FormData at runtime and sends multipart/form-data correctly.
962
+ let json = await Ofetch.ofetch(endpoint, {
963
+ method: "POST",
964
+ body: fd->Obj.magic,
965
+ })
966
+
967
+ switch json->JSON.Decode.object->Option.flatMap(obj => obj->Dict.get("id")) {
968
+ | Some(idJson) =>
969
+ switch idJson->JSON.Decode.string {
970
+ | Some(id) => Ok(id)
971
+ | None => Error("Drive upload: id is not a string")
972
+ }
973
+ | None =>
974
+ let errDetail =
975
+ json
976
+ ->JSON.Decode.object
977
+ ->Option.flatMap(obj => obj->Dict.get("error"))
978
+ ->Option.flatMap(JSON.Decode.object)
979
+ ->Option.flatMap(obj => obj->Dict.get("message"))
980
+ ->Option.flatMap(JSON.Decode.string)
981
+ let statusCode =
982
+ json
983
+ ->JSON.Decode.object
984
+ ->Option.flatMap(obj => obj->Dict.get("error"))
985
+ ->Option.flatMap(JSON.Decode.object)
986
+ ->Option.flatMap(obj => obj->Dict.get("code"))
987
+ ->Option.flatMap(JSON.Decode.string)
988
+ let msg = switch (statusCode, errDetail) {
989
+ | (Some(code), Some(detail)) => code ++ ": " ++ detail
990
+ | (None, Some(detail)) => detail
991
+ | (Some(code), None) => "Upload error: " ++ code
992
+ | (None, None) => "Unknown upload error"
993
+ }
994
+ Error(msg)
995
+ }
996
+ } catch {
997
+ | err =>
998
+ let msg = switch err->JsExn.fromException {
999
+ | Some(jsExn) => JsExn.message(jsExn)->Option.getOr("Drive upload failed")
1000
+ | None => "Drive upload failed"
1001
+ }
1002
+ Error(msg)
1003
+ }
1004
+ }
1005
+ }
1006
+
735
1007
  let default = connect