@kyoji2/raindrop-cli 0.1.1

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.
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@kyoji2/raindrop-cli",
3
+ "version": "0.1.1",
4
+ "description": "AI-native CLI for Raindrop.io",
5
+ "author": "kyoji2",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/kyoji2/raindrop-cli.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/kyoji2/raindrop-cli/issues"
12
+ },
13
+ "homepage": "https://github.com/kyoji2/raindrop-cli#readme",
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "src",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "module": "src/index.ts",
24
+ "type": "module",
25
+ "exports": {
26
+ ".": "./src/api/index.ts",
27
+ "./api": "./src/api/index.ts",
28
+ "./utils": "./src/utils/index.ts"
29
+ },
30
+ "bin": {
31
+ "raindrop": "dist/index.js"
32
+ },
33
+ "scripts": {
34
+ "start": "bun run src/index.ts",
35
+ "dev": "bun --watch src/index.ts",
36
+ "test": "bun test",
37
+ "lint": "biome check ./src ./tests",
38
+ "lint:fix": "biome check --write ./src ./tests",
39
+ "format": "biome format --write ./src ./tests",
40
+ "build": "bun build src/index.ts --outdir dist --target bun && chmod +x dist/index.js",
41
+ "typecheck": "tsc --noEmit"
42
+ },
43
+ "dependencies": {
44
+ "@toon-format/toon": "^2.1.0",
45
+ "commander": "^14.0.2",
46
+ "ora": "^9.0.0",
47
+ "zod": "^4.3.5"
48
+ },
49
+ "devDependencies": {
50
+ "@biomejs/biome": "^2.3.11",
51
+ "@types/bun": "latest"
52
+ },
53
+ "peerDependencies": {
54
+ "typescript": "^5"
55
+ },
56
+ "keywords": [
57
+ "raindrop",
58
+ "bookmarks",
59
+ "cli",
60
+ "ai-native"
61
+ ],
62
+ "license": "MIT"
63
+ }
@@ -0,0 +1,509 @@
1
+ import type { z } from "zod";
2
+ import {
3
+ CollectionResponseSchema,
4
+ CollectionsResponseSchema,
5
+ CoversResponseSchema,
6
+ RaindropResponseSchema,
7
+ RaindropsResponseSchema,
8
+ ResultResponseSchema,
9
+ StatsResponseSchema,
10
+ SuggestionsResponseSchema,
11
+ TagsResponseSchema,
12
+ UserResponseSchema,
13
+ } from "./schemas";
14
+ import type {
15
+ ApiResponse,
16
+ Collection,
17
+ CollectionCreate,
18
+ CollectionUpdate,
19
+ Raindrop,
20
+ RaindropCreate,
21
+ RaindropUpdate,
22
+ RaindropUser,
23
+ Tag,
24
+ UserStats,
25
+ } from "./types";
26
+
27
+ export interface Logger {
28
+ log: (message: string) => void;
29
+ warn: (message: string) => void;
30
+ error: (message: string) => void;
31
+ }
32
+
33
+ const defaultLogger: Logger = {
34
+ log: (msg) => console.log(msg),
35
+ warn: (msg) => console.warn(msg),
36
+ error: (msg) => console.error(msg),
37
+ };
38
+
39
+ export class RaindropError extends Error {
40
+ constructor(
41
+ message: string,
42
+ public statusCode: number = 500,
43
+ public hint?: string,
44
+ ) {
45
+ super(message);
46
+ this.name = "RaindropError";
47
+ }
48
+ }
49
+
50
+ function calculateBackoff(attempt: number, baseMs: number = 1000, maxMs: number = 30000): number {
51
+ const exponential = baseMs * 2 ** attempt;
52
+ const jitter = Math.random() * 1000;
53
+ return Math.min(exponential + jitter, maxMs);
54
+ }
55
+
56
+ function parseResponse<T>(schema: z.ZodType<T>, data: unknown, context: string): T {
57
+ const result = schema.safeParse(data);
58
+ if (!result.success) {
59
+ const issues = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
60
+ throw new RaindropError(`Invalid API response for ${context}: ${issues}`, 500);
61
+ }
62
+ return result.data;
63
+ }
64
+
65
+ export class RaindropAPI {
66
+ private static readonly BASE_URL = "https://api.raindrop.io/rest/v1";
67
+ private static readonly MAX_RETRIES = 3;
68
+ private static readonly REQUEST_TIMEOUT_MS = 60000;
69
+
70
+ constructor(
71
+ private token: string,
72
+ private dryRun: boolean = false,
73
+ private logger: Logger = defaultLogger,
74
+ ) {}
75
+
76
+ private get headers(): Record<string, string> {
77
+ return {
78
+ Authorization: `Bearer ${this.token}`,
79
+ "Content-Type": "application/json",
80
+ };
81
+ }
82
+
83
+ private getDryRunResponse<T>(method: string, path: string): ApiResponse<T> {
84
+ if (method === "DELETE") {
85
+ return { result: true } as ApiResponse<T>;
86
+ }
87
+
88
+ if (path === "/user") {
89
+ return {
90
+ result: true,
91
+ user: { _id: 0, fullName: "Dry Run User" },
92
+ } as ApiResponse<T>;
93
+ }
94
+
95
+ if (path.includes("/tags")) {
96
+ return { result: true, items: [] } as ApiResponse<T>;
97
+ }
98
+
99
+ if (path.includes("/collections") || path.includes("/collection")) {
100
+ return {
101
+ result: true,
102
+ item: { _id: 0, title: "Dry Run Collection", count: 0 } as T,
103
+ items: [],
104
+ };
105
+ }
106
+
107
+ if (path.includes("/raindrop")) {
108
+ return {
109
+ result: true,
110
+ item: {
111
+ _id: 0,
112
+ title: "Dry Run Item",
113
+ link: "http://dryrun.com",
114
+ tags: [],
115
+ } as T,
116
+ items: [],
117
+ };
118
+ }
119
+
120
+ return {
121
+ result: true,
122
+ item: { _id: 0 } as T,
123
+ items: [],
124
+ };
125
+ }
126
+
127
+ private async request<T>(
128
+ method: string,
129
+ path: string,
130
+ options: { json?: unknown; params?: Record<string, string | number> } = {},
131
+ ): Promise<ApiResponse<T>> {
132
+ if (this.dryRun && ["POST", "PUT", "DELETE"].includes(method)) {
133
+ this.logger.log(`[DRY RUN] ${method} ${path}`);
134
+ if (options.json) {
135
+ const filtered = Object.fromEntries(
136
+ Object.entries(options.json as Record<string, unknown>).filter(([k]) => !k.toLowerCase().includes("token")),
137
+ );
138
+ this.logger.log(`Payload: ${JSON.stringify(filtered, null, 2)}`);
139
+ }
140
+ return this.getDryRunResponse<T>(method, path);
141
+ }
142
+
143
+ let url = `${RaindropAPI.BASE_URL}${path}`;
144
+ if (options.params) {
145
+ const searchParams = new URLSearchParams();
146
+ for (const [key, value] of Object.entries(options.params)) {
147
+ searchParams.append(key, String(value));
148
+ }
149
+ url += `?${searchParams.toString()}`;
150
+ }
151
+
152
+ let attempt = 0;
153
+
154
+ while (attempt < RaindropAPI.MAX_RETRIES) {
155
+ const controller = new AbortController();
156
+ const timeoutId = setTimeout(() => controller.abort(), RaindropAPI.REQUEST_TIMEOUT_MS);
157
+
158
+ try {
159
+ const response = await fetch(url, {
160
+ method,
161
+ headers: this.headers,
162
+ body: options.json ? JSON.stringify(options.json) : undefined,
163
+ signal: controller.signal,
164
+ });
165
+
166
+ clearTimeout(timeoutId);
167
+
168
+ if (response.status === 429) {
169
+ attempt++;
170
+ const retryAfter = Number.parseInt(response.headers.get("Retry-After") || "10", 10);
171
+ const backoff = Math.max(retryAfter * 1000, calculateBackoff(attempt));
172
+
173
+ if (attempt >= RaindropAPI.MAX_RETRIES) {
174
+ throw new RaindropError(
175
+ "Rate limit exceeded. Maximum retries reached.",
176
+ 429,
177
+ "Wait a few minutes before trying again.",
178
+ );
179
+ }
180
+
181
+ this.logger.warn(
182
+ `[${method} ${path}] Rate limited. Retrying in ${Math.round(backoff / 1000)}s (attempt ${attempt}/${RaindropAPI.MAX_RETRIES})`,
183
+ );
184
+ await Bun.sleep(backoff);
185
+ continue;
186
+ }
187
+
188
+ if (response.status >= 500) {
189
+ attempt++;
190
+ if (attempt >= RaindropAPI.MAX_RETRIES) {
191
+ throw new RaindropError(
192
+ `Server Error: ${response.status}`,
193
+ response.status,
194
+ "The Raindrop.io server is experiencing issues. Try again later.",
195
+ );
196
+ }
197
+
198
+ const backoff = calculateBackoff(attempt);
199
+ this.logger.warn(
200
+ `[${method} ${path}] Server error ${response.status}. Retrying in ${Math.round(backoff / 1000)}s (attempt ${attempt}/${RaindropAPI.MAX_RETRIES})`,
201
+ );
202
+ await Bun.sleep(backoff);
203
+ continue;
204
+ }
205
+
206
+ if (!response.ok) {
207
+ let errorDetail = await response.text();
208
+ try {
209
+ const errorJson = JSON.parse(errorDetail);
210
+ errorDetail = errorJson.errorMessage || errorDetail;
211
+ } catch {}
212
+
213
+ let hint: string | undefined;
214
+ if (response.status === 401) {
215
+ hint = "Authentication failed. Try running 'raindrop login' again.";
216
+ } else if (response.status === 404) {
217
+ hint = "The requested resource was not found. Verify the ID is correct.";
218
+ } else if (response.status === 400) {
219
+ hint = "Invalid request. Check your input parameters.";
220
+ }
221
+
222
+ throw new RaindropError(`API Error ${response.status}: ${errorDetail}`, response.status, hint);
223
+ }
224
+
225
+ return (await response.json()) as ApiResponse<T>;
226
+ } catch (error) {
227
+ clearTimeout(timeoutId);
228
+
229
+ if (error instanceof RaindropError) throw error;
230
+
231
+ if (error instanceof Error && error.name === "AbortError") {
232
+ throw new RaindropError(
233
+ `Request timeout after ${RaindropAPI.REQUEST_TIMEOUT_MS}ms`,
234
+ 504,
235
+ "The request took too long. Try again later.",
236
+ );
237
+ }
238
+
239
+ attempt++;
240
+ if (attempt >= RaindropAPI.MAX_RETRIES) {
241
+ throw new RaindropError(`Network Error: ${error}`, 503, "Check your internet connection and try again.");
242
+ }
243
+
244
+ const backoff = calculateBackoff(attempt);
245
+ this.logger.warn(
246
+ `[${method} ${path}] Network error. Retrying in ${Math.round(backoff / 1000)}s (attempt ${attempt}/${RaindropAPI.MAX_RETRIES})`,
247
+ );
248
+ await Bun.sleep(backoff);
249
+ }
250
+ }
251
+
252
+ throw new RaindropError("Maximum retries exceeded", 504);
253
+ }
254
+
255
+ async getUser(): Promise<RaindropUser> {
256
+ const data = await this.request<unknown>("GET", "/user");
257
+ const parsed = parseResponse(UserResponseSchema, data, "getUser");
258
+ return parsed.user;
259
+ }
260
+
261
+ async getStats(): Promise<UserStats[]> {
262
+ const data = await this.request<unknown>("GET", "/user/stats");
263
+ const parsed = parseResponse(StatsResponseSchema, data, "getStats");
264
+ return parsed.items;
265
+ }
266
+
267
+ async getCollections(): Promise<Collection[]> {
268
+ const data = await this.request<unknown>("GET", "/collections/all");
269
+ const parsed = parseResponse(CollectionsResponseSchema, data, "getCollections");
270
+ return parsed.items;
271
+ }
272
+
273
+ async getRootCollections(): Promise<Collection[]> {
274
+ const data = await this.request<unknown>("GET", "/collections");
275
+ const parsed = parseResponse(CollectionsResponseSchema, data, "getRootCollections");
276
+ return parsed.items;
277
+ }
278
+
279
+ async getChildCollections(): Promise<Collection[]> {
280
+ const data = await this.request<unknown>("GET", "/collections/childrens");
281
+ const parsed = parseResponse(CollectionsResponseSchema, data, "getChildCollections");
282
+ return parsed.items;
283
+ }
284
+
285
+ async getCollection(id: number): Promise<Collection> {
286
+ const data = await this.request<unknown>("GET", `/collection/${id}`);
287
+ const parsed = parseResponse(CollectionResponseSchema, data, "getCollection");
288
+ return parsed.item;
289
+ }
290
+
291
+ async createCollection(collection: CollectionCreate): Promise<Collection> {
292
+ const data = await this.request<unknown>("POST", "/collection", {
293
+ json: collection,
294
+ });
295
+ const parsed = parseResponse(CollectionResponseSchema, data, "createCollection");
296
+ return parsed.item;
297
+ }
298
+
299
+ async updateCollection(id: number, update: CollectionUpdate): Promise<Collection> {
300
+ const data = await this.request<unknown>("PUT", `/collection/${id}`, {
301
+ json: update,
302
+ });
303
+ const parsed = parseResponse(CollectionResponseSchema, data, "updateCollection");
304
+ return parsed.item;
305
+ }
306
+
307
+ async deleteCollection(id: number): Promise<boolean> {
308
+ const data = await this.request<unknown>("DELETE", `/collection/${id}`);
309
+ const parsed = parseResponse(ResultResponseSchema, data, "deleteCollection");
310
+ return parsed.result;
311
+ }
312
+
313
+ async deleteCollections(ids: number[]): Promise<boolean> {
314
+ const data = await this.request<unknown>("DELETE", "/collections", {
315
+ json: { ids },
316
+ });
317
+ const parsed = parseResponse(ResultResponseSchema, data, "deleteCollections");
318
+ return parsed.result;
319
+ }
320
+
321
+ async reorderCollections(sort: string): Promise<boolean> {
322
+ const data = await this.request<unknown>("PUT", "/collections", {
323
+ json: { sort },
324
+ });
325
+ const parsed = parseResponse(ResultResponseSchema, data, "reorderCollections");
326
+ return parsed.result;
327
+ }
328
+
329
+ async expandAllCollections(expanded: boolean): Promise<boolean> {
330
+ const data = await this.request<unknown>("PUT", "/collections", {
331
+ json: { expanded },
332
+ });
333
+ const parsed = parseResponse(ResultResponseSchema, data, "expandAllCollections");
334
+ return parsed.result;
335
+ }
336
+
337
+ async mergeCollections(ids: number[], targetId: number): Promise<boolean> {
338
+ const data = await this.request<unknown>("PUT", "/collections/merge", {
339
+ json: { ids, to: targetId },
340
+ });
341
+ const parsed = parseResponse(ResultResponseSchema, data, "mergeCollections");
342
+ return parsed.result;
343
+ }
344
+
345
+ async cleanEmptyCollections(): Promise<number> {
346
+ const data = await this.request<unknown>("PUT", "/collections/clean");
347
+ const parsed = parseResponse(ResultResponseSchema, data, "cleanEmptyCollections");
348
+ return parsed.count || 0;
349
+ }
350
+
351
+ async uploadCollectionCover(id: number, filePath: string): Promise<Collection> {
352
+ if (this.dryRun) {
353
+ this.logger.log(`[DRY RUN] PUT /collection/${id}/cover`);
354
+ this.logger.log(`File: ${filePath}`);
355
+ return { _id: id, title: "Dry Run Icon", count: 0 };
356
+ }
357
+
358
+ const file = Bun.file(filePath);
359
+ const formData = new FormData();
360
+ formData.append("cover", file);
361
+
362
+ const response = await fetch(`${RaindropAPI.BASE_URL}/collection/${id}/cover`, {
363
+ method: "PUT",
364
+ headers: { Authorization: `Bearer ${this.token}` },
365
+ body: formData,
366
+ });
367
+
368
+ if (!response.ok) {
369
+ throw new RaindropError(`Upload failed: ${response.status}`, response.status);
370
+ }
371
+
372
+ const data = await response.json();
373
+ const parsed = parseResponse(CollectionResponseSchema, data, "uploadCollectionCover");
374
+ return parsed.item;
375
+ }
376
+
377
+ async searchCovers(query: string): Promise<string[]> {
378
+ const data = await this.request<unknown>("GET", `/collections/covers/${encodeURIComponent(query)}`);
379
+ const parsed = parseResponse(CoversResponseSchema, data, "searchCovers");
380
+ const icons: string[] = [];
381
+ for (const group of parsed.items) {
382
+ for (const icon of group.icons || []) {
383
+ if (icon.png) icons.push(icon.png);
384
+ }
385
+ }
386
+ return icons;
387
+ }
388
+
389
+ async emptyTrash(): Promise<boolean> {
390
+ const data = await this.request<unknown>("DELETE", "/collection/-99");
391
+ const parsed = parseResponse(ResultResponseSchema, data, "emptyTrash");
392
+ return parsed.result;
393
+ }
394
+
395
+ async getTags(collectionId: number = 0): Promise<Tag[]> {
396
+ const data = await this.request<unknown>("GET", `/tags/${collectionId}`);
397
+ const parsed = parseResponse(TagsResponseSchema, data, "getTags");
398
+ return parsed.items;
399
+ }
400
+
401
+ async deleteTags(tags: string[], collectionId: number = 0): Promise<boolean> {
402
+ const data = await this.request<unknown>("DELETE", `/tags/${collectionId}`, {
403
+ json: { tags },
404
+ });
405
+ const parsed = parseResponse(ResultResponseSchema, data, "deleteTags");
406
+ return parsed.result;
407
+ }
408
+
409
+ async renameTag(oldName: string, newName: string, collectionId: number = 0): Promise<boolean> {
410
+ const data = await this.request<unknown>("PUT", `/tags/${collectionId}`, {
411
+ json: { replace: newName, tags: [oldName] },
412
+ });
413
+ const parsed = parseResponse(ResultResponseSchema, data, "renameTag");
414
+ return parsed.result;
415
+ }
416
+
417
+ async search(query: string = "", collectionId: number = 0, limit: number = 50): Promise<Raindrop[]> {
418
+ const allItems: Raindrop[] = [];
419
+ let page = 0;
420
+ const perPage = Math.min(limit, 50);
421
+
422
+ while (allItems.length < limit) {
423
+ const data = await this.request<unknown>("GET", `/raindrops/${collectionId}`, {
424
+ params: { search: query, page, perpage: perPage },
425
+ });
426
+ const parsed = parseResponse(RaindropsResponseSchema, data, "search");
427
+
428
+ const items = parsed.items;
429
+ if (items.length === 0) break;
430
+
431
+ const remaining = limit - allItems.length;
432
+ allItems.push(...items.slice(0, remaining));
433
+
434
+ if (items.length < perPage) break;
435
+ page++;
436
+ }
437
+
438
+ return allItems;
439
+ }
440
+
441
+ async getRaindrop(id: number): Promise<Raindrop> {
442
+ const data = await this.request<unknown>("GET", `/raindrop/${id}`);
443
+ const parsed = parseResponse(RaindropResponseSchema, data, "getRaindrop");
444
+ return parsed.item;
445
+ }
446
+
447
+ async addRaindrop(raindrop: RaindropCreate): Promise<Raindrop> {
448
+ const data = await this.request<unknown>("POST", "/raindrop", {
449
+ json: raindrop,
450
+ });
451
+ const parsed = parseResponse(RaindropResponseSchema, data, "addRaindrop");
452
+ return parsed.item;
453
+ }
454
+
455
+ async updateRaindrop(id: number, update: RaindropUpdate): Promise<Raindrop> {
456
+ const data = await this.request<unknown>("PUT", `/raindrop/${id}`, {
457
+ json: update,
458
+ });
459
+ const parsed = parseResponse(RaindropResponseSchema, data, "updateRaindrop");
460
+ return parsed.item;
461
+ }
462
+
463
+ async deleteRaindrop(id: number): Promise<boolean> {
464
+ const data = await this.request<unknown>("DELETE", `/raindrop/${id}`);
465
+ const parsed = parseResponse(ResultResponseSchema, data, "deleteRaindrop");
466
+ return parsed.result;
467
+ }
468
+
469
+ async batchUpdateRaindrops(collectionId: number, ids: number[], update: RaindropUpdate): Promise<boolean> {
470
+ const payload = { ...update, ids };
471
+ const data = await this.request<unknown>("PUT", `/raindrops/${collectionId}`, {
472
+ json: payload,
473
+ });
474
+ const parsed = parseResponse(ResultResponseSchema, data, "batchUpdateRaindrops");
475
+ return parsed.result;
476
+ }
477
+
478
+ async batchDeleteRaindrops(collectionId: number, ids: number[]): Promise<boolean> {
479
+ const data = await this.request<unknown>("DELETE", `/raindrops/${collectionId}`, {
480
+ json: { ids },
481
+ });
482
+ const parsed = parseResponse(ResultResponseSchema, data, "batchDeleteRaindrops");
483
+ return parsed.result;
484
+ }
485
+
486
+ async getSuggestions(id: number): Promise<{ tags?: string[]; collections?: Collection[] }> {
487
+ const data = await this.request<unknown>("GET", `/raindrop/${id}/suggest`);
488
+ const parsed = parseResponse(SuggestionsResponseSchema, data, "getSuggestions");
489
+ const item = parsed.item || {};
490
+ return {
491
+ ...item,
492
+ tags: item.tags ?? undefined,
493
+ collections: item.collections ?? undefined,
494
+ };
495
+ }
496
+
497
+ async checkWayback(url: string): Promise<string | null> {
498
+ try {
499
+ const response = await fetch(`https://archive.org/wayback/available?url=${encodeURIComponent(url)}`);
500
+ if (response.ok) {
501
+ const data = (await response.json()) as {
502
+ archived_snapshots?: { closest?: { url?: string } };
503
+ };
504
+ return data.archived_snapshots?.closest?.url ?? null;
505
+ }
506
+ } catch {}
507
+ return null;
508
+ }
509
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./client";
2
+ export * from "./types";
@@ -0,0 +1,125 @@
1
+ import { z } from "zod";
2
+
3
+ export const RaindropUserSchema = z.object({
4
+ _id: z.number(),
5
+ fullName: z.string(),
6
+ email: z.string().nullish(),
7
+ avatar: z.string().nullish(),
8
+ pro: z.boolean().nullish(),
9
+ });
10
+
11
+ export const CollectionSchema = z.object({
12
+ _id: z.number(),
13
+ title: z.string(),
14
+ count: z.number(),
15
+ parent: z.object({ $id: z.number() }).nullish(),
16
+ cover: z.array(z.string()).nullish(),
17
+ color: z.string().nullish(),
18
+ view: z.enum(["list", "simple", "grid", "masonry"]).nullish(),
19
+ public: z.boolean().nullish(),
20
+ expanded: z.boolean().nullish(),
21
+ lastUpdate: z.string().nullish(),
22
+ created: z.string().nullish(),
23
+ sort: z.number().nullish(),
24
+ });
25
+
26
+ export const HighlightSchema = z.object({
27
+ _id: z.string(),
28
+ text: z.string(),
29
+ note: z.string().nullish(),
30
+ color: z.string().nullish(),
31
+ created: z.string().nullish(),
32
+ });
33
+
34
+ export const MediaSchema = z.object({
35
+ link: z.string(),
36
+ type: z.string(),
37
+ });
38
+
39
+ export const RaindropSchema = z.object({
40
+ _id: z.number(),
41
+ title: z.string(),
42
+ link: z.string(),
43
+ excerpt: z.string().nullish(),
44
+ note: z.string().nullish(),
45
+ type: z.enum(["link", "article", "image", "video", "document", "audio"]).nullish(),
46
+ tags: z.array(z.string()),
47
+ cover: z.string().nullish(),
48
+ domain: z.string().nullish(),
49
+ created: z.string().nullish(),
50
+ lastUpdate: z.string().nullish(),
51
+ collection: z.object({ $id: z.number() }).nullish(),
52
+ highlights: z.array(HighlightSchema).nullish(),
53
+ important: z.boolean().nullish(),
54
+ removed: z.boolean().nullish(),
55
+ media: z.array(MediaSchema).nullish(),
56
+ });
57
+
58
+ export const TagSchema = z.object({
59
+ _id: z.string(),
60
+ count: z.number().nullish(),
61
+ });
62
+
63
+ export const UserStatsSchema = z.object({
64
+ _id: z.number(),
65
+ count: z.number(),
66
+ });
67
+
68
+ export const UserResponseSchema = z.object({
69
+ result: z.boolean(),
70
+ user: RaindropUserSchema,
71
+ });
72
+
73
+ export const CollectionResponseSchema = z.object({
74
+ result: z.boolean(),
75
+ item: CollectionSchema,
76
+ });
77
+
78
+ export const CollectionsResponseSchema = z.object({
79
+ result: z.boolean(),
80
+ items: z.array(CollectionSchema),
81
+ });
82
+
83
+ export const RaindropResponseSchema = z.object({
84
+ result: z.boolean(),
85
+ item: RaindropSchema,
86
+ });
87
+
88
+ export const RaindropsResponseSchema = z.object({
89
+ result: z.boolean(),
90
+ items: z.array(RaindropSchema),
91
+ });
92
+
93
+ export const TagsResponseSchema = z.object({
94
+ result: z.boolean(),
95
+ items: z.array(TagSchema),
96
+ });
97
+
98
+ export const StatsResponseSchema = z.object({
99
+ result: z.boolean(),
100
+ items: z.array(UserStatsSchema),
101
+ });
102
+
103
+ export const ResultResponseSchema = z.object({
104
+ result: z.boolean(),
105
+ count: z.number().nullish(),
106
+ });
107
+
108
+ export const SuggestionsResponseSchema = z.object({
109
+ result: z.boolean(),
110
+ item: z
111
+ .object({
112
+ tags: z.array(z.string()).nullish(),
113
+ collections: z.array(CollectionSchema).nullish(),
114
+ })
115
+ .nullish(),
116
+ });
117
+
118
+ export const CoversResponseSchema = z.object({
119
+ result: z.boolean(),
120
+ items: z.array(
121
+ z.object({
122
+ icons: z.array(z.object({ png: z.string() })).nullish(),
123
+ }),
124
+ ),
125
+ });