@f3liz/rescript-misskey-api 0.7.0 → 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.
@@ -71,8 +71,27 @@ async function request(client, endpoint, paramsOpt, param) {
71
71
  };
72
72
  } catch (raw_err) {
73
73
  let err = Primitive_exceptions.internalToException(raw_err);
74
+ let errorData = err.data;
74
75
  let jsExn = Stdlib_JsExn.fromException(err);
75
- let msg = jsExn !== undefined ? Stdlib_Option.getOr(Stdlib_JsExn.message(Primitive_option.valFromOption(jsExn)), "Unknown error") : "Unknown error";
76
+ let baseMsg = jsExn !== undefined ? Stdlib_Option.getOr(Stdlib_JsExn.message(Primitive_option.valFromOption(jsExn)), "Unknown error") : "Unknown error";
77
+ let msg;
78
+ if (errorData !== undefined) {
79
+ let obj = Stdlib_JSON.Decode.object(errorData);
80
+ if (obj !== undefined) {
81
+ let errObj = Stdlib_Option.flatMap(obj["error"], Stdlib_JSON.Decode.object);
82
+ if (errObj !== undefined) {
83
+ let code = Stdlib_Option.getOr(Stdlib_Option.flatMap(errObj["code"], Stdlib_JSON.Decode.string), "");
84
+ let message = Stdlib_Option.getOr(Stdlib_Option.flatMap(errObj["message"], Stdlib_JSON.Decode.string), "");
85
+ msg = baseMsg + ` [` + code + `] ` + message;
86
+ } else {
87
+ msg = baseMsg;
88
+ }
89
+ } else {
90
+ msg = baseMsg;
91
+ }
92
+ } else {
93
+ msg = baseMsg;
94
+ }
76
95
  return {
77
96
  TAG: "Error",
78
97
  _0: msg
@@ -88,12 +107,16 @@ function origin(client) {
88
107
  return client.origin;
89
108
  }
90
109
 
110
+ function token(client) {
111
+ return client.token;
112
+ }
113
+
91
114
  function close(client) {
92
115
  Stdlib_Option.forEach(client.streamClient, StreamClient.close);
93
116
  client.streamClient = undefined;
94
117
  }
95
118
 
96
- function create(client, text, visibilityOpt, cw, localOnlyOpt, replyId, renoteId, param) {
119
+ function create(client, text, visibilityOpt, cw, localOnlyOpt, replyId, renoteId, fileIds, param) {
97
120
  let visibility = visibilityOpt !== undefined ? visibilityOpt : "public";
98
121
  let localOnly = localOnlyOpt !== undefined ? localOnlyOpt : false;
99
122
  let params = {};
@@ -114,6 +137,9 @@ function create(client, text, visibilityOpt, cw, localOnlyOpt, replyId, renoteId
114
137
  Stdlib_Option.forEach(renoteId, v => {
115
138
  params["renoteId"] = v;
116
139
  });
140
+ Stdlib_Option.forEach(fileIds, ids => {
141
+ params["fileIds"] = ids.map(prim => prim);
142
+ });
117
143
  return request(client, "notes/create", params, undefined);
118
144
  }
119
145
 
@@ -198,13 +224,83 @@ function unreact(client, noteId) {
198
224
  return request(client, "notes/reactions/delete", params, undefined);
199
225
  }
200
226
 
227
+ function show(client, noteId) {
228
+ let params = {};
229
+ params["noteId"] = noteId;
230
+ return request(client, "notes/show", params, undefined);
231
+ }
232
+
233
+ function children(client, noteId, limitOpt, sinceId, untilId, param) {
234
+ let limit = limitOpt !== undefined ? limitOpt : 30;
235
+ let params = {};
236
+ params["noteId"] = noteId;
237
+ params["limit"] = limit;
238
+ Stdlib_Option.forEach(sinceId, v => {
239
+ params["sinceId"] = v;
240
+ });
241
+ Stdlib_Option.forEach(untilId, v => {
242
+ params["untilId"] = v;
243
+ });
244
+ return request(client, "notes/children", params, undefined);
245
+ }
246
+
247
+ function conversation(client, noteId, limitOpt, param) {
248
+ let limit = limitOpt !== undefined ? limitOpt : 30;
249
+ let params = {};
250
+ params["noteId"] = noteId;
251
+ params["limit"] = limit;
252
+ return request(client, "notes/conversation", params, undefined);
253
+ }
254
+
201
255
  let Notes = {
202
256
  create: create,
203
257
  $$delete: $$delete,
204
258
  fetch: fetch,
205
259
  timeline: fetch,
206
260
  react: react,
207
- unreact: unreact
261
+ unreact: unreact,
262
+ show: show,
263
+ children: children,
264
+ conversation: conversation
265
+ };
266
+
267
+ function show$1(client, userId, username, host, param) {
268
+ let params = {};
269
+ Stdlib_Option.forEach(userId, v => {
270
+ params["userId"] = v;
271
+ });
272
+ Stdlib_Option.forEach(username, v => {
273
+ params["username"] = v;
274
+ });
275
+ Stdlib_Option.forEach(host, v => {
276
+ params["host"] = v;
277
+ });
278
+ return request(client, "users/show", params, undefined);
279
+ }
280
+
281
+ function notes(client, userId, limitOpt, withRepliesOpt, withRenotesOpt, withFilesOpt, sinceId, untilId, param) {
282
+ let limit = limitOpt !== undefined ? limitOpt : 20;
283
+ let withReplies = withRepliesOpt !== undefined ? withRepliesOpt : false;
284
+ let withRenotes = withRenotesOpt !== undefined ? withRenotesOpt : true;
285
+ let withFiles = withFilesOpt !== undefined ? withFilesOpt : false;
286
+ let params = {};
287
+ params["userId"] = userId;
288
+ params["limit"] = limit;
289
+ params["withReplies"] = withReplies;
290
+ params["withRenotes"] = withRenotes;
291
+ params["withFiles"] = withFiles;
292
+ Stdlib_Option.forEach(sinceId, v => {
293
+ params["sinceId"] = v;
294
+ });
295
+ Stdlib_Option.forEach(untilId, v => {
296
+ params["untilId"] = v;
297
+ });
298
+ return request(client, "users/notes", params, undefined);
299
+ }
300
+
301
+ let Users = {
302
+ show: show$1,
303
+ notes: notes
208
304
  };
209
305
 
210
306
  function ensureStream(client) {
@@ -614,6 +710,185 @@ function isPermissionDenied(error) {
614
710
  return match === "PERMISSION_DENIED";
615
711
  }
616
712
 
713
+ async function get(client) {
714
+ let json = await request(client, "meta", undefined, undefined);
715
+ if (json.TAG !== "Ok") {
716
+ return {
717
+ TAG: "Error",
718
+ _0: json._0
719
+ };
720
+ }
721
+ let obj = Stdlib_JSON.Decode.object(json._0);
722
+ if (obj !== undefined) {
723
+ return {
724
+ TAG: "Ok",
725
+ _0: {
726
+ swPublickey: Stdlib_Option.flatMap(obj["swPublickey"], Stdlib_JSON.Decode.string)
727
+ }
728
+ };
729
+ } else {
730
+ return {
731
+ TAG: "Error",
732
+ _0: "Invalid meta response"
733
+ };
734
+ }
735
+ }
736
+
737
+ let Meta = {
738
+ get: get
739
+ };
740
+
741
+ async function register(client, endpoint, auth, publickey, sendReadMessageOpt, param) {
742
+ let sendReadMessage = sendReadMessageOpt !== undefined ? sendReadMessageOpt : false;
743
+ let params = {};
744
+ params["endpoint"] = endpoint;
745
+ params["auth"] = auth;
746
+ params["publickey"] = publickey;
747
+ params["sendReadMessage"] = sendReadMessage;
748
+ let json = await request(client, "sw/register", params, undefined);
749
+ if (json.TAG !== "Ok") {
750
+ return {
751
+ TAG: "Error",
752
+ _0: json._0
753
+ };
754
+ }
755
+ let obj = Stdlib_JSON.Decode.object(json._0);
756
+ if (obj !== undefined) {
757
+ return {
758
+ TAG: "Ok",
759
+ _0: {
760
+ state: Stdlib_Option.flatMap(obj["state"], Stdlib_JSON.Decode.string),
761
+ key: Stdlib_Option.flatMap(obj["key"], Stdlib_JSON.Decode.string),
762
+ userId: Stdlib_Option.getOr(Stdlib_Option.flatMap(obj["userId"], Stdlib_JSON.Decode.string), ""),
763
+ endpoint: Stdlib_Option.getOr(Stdlib_Option.flatMap(obj["endpoint"], Stdlib_JSON.Decode.string), ""),
764
+ sendReadMessage: Stdlib_Option.getOr(Stdlib_Option.flatMap(obj["sendReadMessage"], Stdlib_JSON.Decode.bool), false)
765
+ }
766
+ };
767
+ } else {
768
+ return {
769
+ TAG: "Error",
770
+ _0: "Invalid sw/register response"
771
+ };
772
+ }
773
+ }
774
+
775
+ async function unregister(client, endpoint) {
776
+ let params = {};
777
+ params["endpoint"] = endpoint;
778
+ let e = await request(client, "sw/unregister", params, undefined);
779
+ if (e.TAG === "Ok") {
780
+ return {
781
+ TAG: "Ok",
782
+ _0: undefined
783
+ };
784
+ } else {
785
+ return {
786
+ TAG: "Error",
787
+ _0: e._0
788
+ };
789
+ }
790
+ }
791
+
792
+ let Sw = {
793
+ register: register,
794
+ unregister: unregister
795
+ };
796
+
797
+ async function create$1(client, name, url, secret, on, param) {
798
+ let params = {};
799
+ params["name"] = name;
800
+ params["url"] = url;
801
+ params["secret"] = secret;
802
+ params["on"] = on.map(prim => prim);
803
+ let json = await request(client, "i/webhooks/create", params, undefined);
804
+ if (json.TAG !== "Ok") {
805
+ return {
806
+ TAG: "Error",
807
+ _0: json._0
808
+ };
809
+ }
810
+ let obj = Stdlib_JSON.Decode.object(json._0);
811
+ if (obj === undefined) {
812
+ return {
813
+ TAG: "Error",
814
+ _0: "Invalid webhook response"
815
+ };
816
+ }
817
+ let id = Stdlib_Option.getOr(Stdlib_Option.flatMap(obj["id"], Stdlib_JSON.Decode.string), "");
818
+ let name$1 = Stdlib_Option.getOr(Stdlib_Option.flatMap(obj["name"], Stdlib_JSON.Decode.string), "");
819
+ let url$1 = Stdlib_Option.getOr(Stdlib_Option.flatMap(obj["url"], Stdlib_JSON.Decode.string), "");
820
+ let active = Stdlib_Option.getOr(Stdlib_Option.flatMap(obj["active"], Stdlib_JSON.Decode.bool), false);
821
+ return {
822
+ TAG: "Ok",
823
+ _0: {
824
+ id: id,
825
+ name: name$1,
826
+ url: url$1,
827
+ active: active
828
+ }
829
+ };
830
+ }
831
+
832
+ async function $$delete$1(client, webhookId) {
833
+ let params = {};
834
+ params["webhookId"] = webhookId;
835
+ let e = await request(client, "i/webhooks/delete", params, undefined);
836
+ if (e.TAG === "Ok") {
837
+ return {
838
+ TAG: "Ok",
839
+ _0: undefined
840
+ };
841
+ } else {
842
+ return {
843
+ TAG: "Error",
844
+ _0: e._0
845
+ };
846
+ }
847
+ }
848
+
849
+ async function list$1(client) {
850
+ let json = await request(client, "i/webhooks/list", undefined, undefined);
851
+ if (json.TAG !== "Ok") {
852
+ return {
853
+ TAG: "Error",
854
+ _0: json._0
855
+ };
856
+ }
857
+ let arr = Stdlib_JSON.Decode.array(json._0);
858
+ if (arr !== undefined) {
859
+ return {
860
+ TAG: "Ok",
861
+ _0: Stdlib_Array.filterMap(arr, item => {
862
+ let obj = Stdlib_JSON.Decode.object(item);
863
+ if (obj === undefined) {
864
+ return;
865
+ }
866
+ let id = Stdlib_Option.getOr(Stdlib_Option.flatMap(obj["id"], Stdlib_JSON.Decode.string), "");
867
+ let name = Stdlib_Option.getOr(Stdlib_Option.flatMap(obj["name"], Stdlib_JSON.Decode.string), "");
868
+ let url = Stdlib_Option.getOr(Stdlib_Option.flatMap(obj["url"], Stdlib_JSON.Decode.string), "");
869
+ let active = Stdlib_Option.getOr(Stdlib_Option.flatMap(obj["active"], Stdlib_JSON.Decode.bool), false);
870
+ return {
871
+ id: id,
872
+ name: name,
873
+ url: url,
874
+ active: active
875
+ };
876
+ })
877
+ };
878
+ } else {
879
+ return {
880
+ TAG: "Ok",
881
+ _0: []
882
+ };
883
+ }
884
+ }
885
+
886
+ let Webhooks = {
887
+ create: create$1,
888
+ $$delete: $$delete$1,
889
+ list: list$1
890
+ };
891
+
617
892
  function isAPIError(error) {
618
893
  let obj = Stdlib_JSON.Decode.object(error);
619
894
  if (obj === undefined) {
@@ -631,6 +906,61 @@ function isAPIError(error) {
631
906
  }
632
907
  }
633
908
 
909
+ async function upload(client, file, sensitiveOpt, param) {
910
+ let sensitive = sensitiveOpt !== undefined ? sensitiveOpt : false;
911
+ try {
912
+ let fd = (new FormData());
913
+ fd.append("file", file);
914
+ Stdlib_Option.forEach(client.token, tok => fd.append("i", tok));
915
+ if (sensitive) {
916
+ fd.append("isSensitive", "true");
917
+ }
918
+ let endpoint = client.origin + "/api/drive/files/create";
919
+ let json = await Ofetch.ofetch(endpoint, {
920
+ method: "POST",
921
+ body: fd
922
+ });
923
+ let idJson = Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), obj => obj["id"]);
924
+ if (idJson !== undefined) {
925
+ let id = Stdlib_JSON.Decode.string(idJson);
926
+ if (id !== undefined) {
927
+ return {
928
+ TAG: "Ok",
929
+ _0: id
930
+ };
931
+ } else {
932
+ return {
933
+ TAG: "Error",
934
+ _0: "Drive upload: id is not a string"
935
+ };
936
+ }
937
+ }
938
+ let errDetail = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), obj => obj["error"]), Stdlib_JSON.Decode.object), obj => obj["message"]), Stdlib_JSON.Decode.string);
939
+ let statusCode = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), obj => obj["error"]), Stdlib_JSON.Decode.object), obj => obj["code"]), Stdlib_JSON.Decode.string);
940
+ let msg = statusCode !== undefined ? (
941
+ errDetail !== undefined ? statusCode + ": " + errDetail : "Upload error: " + statusCode
942
+ ) : (
943
+ errDetail !== undefined ? errDetail : "Unknown upload error"
944
+ );
945
+ return {
946
+ TAG: "Error",
947
+ _0: msg
948
+ };
949
+ } catch (raw_err) {
950
+ let err = Primitive_exceptions.internalToException(raw_err);
951
+ let jsExn = Stdlib_JsExn.fromException(err);
952
+ let msg$1 = jsExn !== undefined ? Stdlib_Option.getOr(Stdlib_JsExn.message(Primitive_option.valFromOption(jsExn)), "Drive upload failed") : "Drive upload failed";
953
+ return {
954
+ TAG: "Error",
955
+ _0: msg$1
956
+ };
957
+ }
958
+ }
959
+
960
+ let Drive = {
961
+ upload: upload
962
+ };
963
+
634
964
  let $$default = connect;
635
965
 
636
966
  export {
@@ -641,14 +971,20 @@ export {
641
971
  request,
642
972
  currentUser,
643
973
  origin,
974
+ token,
644
975
  close,
645
976
  Notes,
977
+ Users,
646
978
  Stream,
647
979
  Emojis,
648
980
  CustomTimelines,
649
981
  MiAuth,
650
982
  isPermissionDenied,
983
+ Meta,
984
+ Sw,
985
+ Webhooks,
651
986
  isAPIError,
987
+ Drive,
652
988
  $$default as default,
653
989
  }
654
990
  /* Not a pure module */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@f3liz/rescript-misskey-api",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "ReScript bindings for Misskey API using OpenAPI code generation",
5
5
  "keywords": [
6
6
  "misskey",
package/src/Misskey.res CHANGED
@@ -117,10 +117,29 @@ let request = async (
117
117
  Ok(json)
118
118
  } catch {
119
119
  | err =>
120
- let msg = switch err->JsExn.fromException {
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 {
121
123
  | Some(jsExn) => JsExn.message(jsExn)->Option.getOr("Unknown error")
122
124
  | None => "Unknown error"
123
125
  }
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
+ }
124
143
  Error(msg)
125
144
  }
126
145
  }
@@ -133,6 +152,9 @@ let currentUser = (client: t): promise<result<JSON.t, string>> => {
133
152
  /// Get client origin (instance URL).
134
153
  let origin = (client: t): string => client.origin
135
154
 
155
+ /// Get client authentication token.
156
+ let token = (client: t): option<string> => client.token
157
+
136
158
  /// Close client and cleanup (close streaming connections).
137
159
  let close = (client: t): unit => {
138
160
  client.streamClient->Option.forEach(s => s->StreamClient.close)
@@ -165,6 +187,7 @@ module Notes = {
165
187
  ~localOnly: bool=false,
166
188
  ~replyId: option<string>=?,
167
189
  ~renoteId: option<string>=?,
190
+ ~fileIds: option<array<string>>=?,
168
191
  (),
169
192
  ): promise<result<JSON.t, string>> => {
170
193
  let params = Dict.make()
@@ -182,6 +205,9 @@ module Notes = {
182
205
  cw->Option.forEach(v => params->Dict.set("cw", v->JSON.Encode.string))
183
206
  replyId->Option.forEach(v => params->Dict.set("replyId", v->JSON.Encode.string))
184
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
+ )
185
211
 
186
212
  request(client, "notes/create", ~params=params->JSON.Encode.object, ())
187
213
  }
@@ -257,12 +283,89 @@ module Notes = {
257
283
  params->Dict.set("noteId", noteId->JSON.Encode.string)
258
284
  request(client, "notes/reactions/delete", ~params=params->JSON.Encode.object, ())
259
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
+ }
260
323
  }
261
324
 
262
325
  // ============================================================================
263
- // Stream API - Real-time updates via WebSocket
326
+ // Users API
264
327
  // ============================================================================
265
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
+
266
369
  module Stream = {
267
370
  type subscription = {dispose: unit => unit}
268
371
 
@@ -660,6 +763,166 @@ let isPermissionDenied = (error: JSON.t): bool => {
660
763
  }
661
764
 
662
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
+
663
926
  let isAPIError = (error: JSON.t): option<apiError> => {
664
927
  switch error->JSON.Decode.object {
665
928
  | Some(obj) =>
@@ -675,4 +938,70 @@ let isAPIError = (error: JSON.t): option<apiError> => {
675
938
  }
676
939
  }
677
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
+
678
1007
  let default = connect