···6677import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";
88import { sql } from "drizzle-orm";
99+import type { TagRef } from "../api/types.js";
9101011// WebStorm keeps throwing errors with the default statements as it wants
1112// an actual SQLite query, despite being valid. Sucks.
···2324 unlisted: int("unlisted", { mode: "boolean" }).notNull(),
2425 notes: text("notes"),
2526 tags: text("tags", { mode: "json" })
2626- .$type<string[]>()
2727+ .$type<TagRef[]>()
2728 .default(sql`'[]'`),
2829 unread: int("unread", { mode: "boolean" }),
2930 languages: text("languages", { mode: "json" })
···6364 .default(sql`(unixepoch() * 1000)`),
6465 did: text("did").notNull().unique(),
6566 cid: text("cid").notNull(),
6666- displayName: text("displayName"),
6767+ displayName: text("displayName").notNull(),
6768 description: text("description"),
6869 avatar: text("avatar"),
6970 createdAt: int("createdAt", { mode: "timestamp_ms" })
+21
backend/src/hasher.ts
···11+/*
22+ * clippr: a social bookmarking service for the AT Protocol
33+ * Copyright (c) 2025 clippr contributors.
44+ * SPDX-License-Identifier: AGPL-3.0-only
55+ */
66+77+import xxhash from "xxhash-wasm";
88+99+/// Hash a given string into a hexadecimal xxh64 string.
1010+export async function hashString(data: string): Promise<string> {
1111+ const { h64 } = await xxhash();
1212+ return h64(data).toString(16);
1313+}
1414+1515+/// Check if a string is equivalent to a given hash.
1616+export async function validateHash(
1717+ data: string,
1818+ hash: string,
1919+): Promise<boolean> {
2020+ return hash === (await hashString(data));
2121+}
+2-3
backend/src/network/commit.ts
···1616import Logger from "../logger.js";
1717import { isBlob } from "@atcute/lexicons/interfaces";
1818import { validateClip, validateProfile, validateTag } from "./validator.js";
1919-import xxhash from "xxhash-wasm";
2019import { convertDidToString } from "./converters.js";
2020+import { hashString } from "../hasher.js";
21212222const db = Database.getInstance().getDb();
2323···6565 };
66666767 // xxh64, NOT xxh3 learned that the hard way
6868- const { h64 } = await xxhash();
6969- const urlHash = h64(record.url).toString(16);
6868+ const urlHash: string = await hashString(record.url);
70697170 if (urlHash !== event.commit.rkey) {
7271 Logger.verbose(
-9
backend/src/network/converters.ts
···3131 }
3232}
33333434-// TODO: Stop leeching off the Bluesky CDN and get the blob directly from the user's PDS
3535-// Get a CDN URI from a blob's CID
3636-export async function getUriFromBlobCid(
3737- did: string,
3838- cid: string,
3939-): Promise<string> {
4040- return `https://cdn.bsky.app/img/avatar/plain/${did}/${cid}`;
4141-}
4242-4334// Get a user's handle from their DID. DID method agnostic.
4435export async function getHandleFromDid(did: string): Promise<string> {
4536 const docResolver = new CompositeDidDocumentResolver({
+114-6
backend/src/routes/xrpc.ts
···88import { Database } from "../db/database.js";
99import { usersTable } from "../db/schema.js";
1010import { eq } from "drizzle-orm";
1111-import {
1212- getDidFromHandle,
1313- getHandleFromDid,
1414- getUriFromBlobCid,
1515-} from "../network/converters.js";
1111+import { getDidFromHandle, getHandleFromDid } from "../network/converters.js";
1212+import { createClipView } from "../api/feed.js";
1313+import { type ClipViewQuery, type ErrorResponse } from "../api/types.js";
1414+import { createAvatarLink } from "../api/profile.js";
1515+import { SocialClipprFeedDefs } from "@clipprjs/lexicons";
1616+import { is } from "@atcute/lexicons";
16171718const app = new Hono();
1819const db = Database.getInstance().getDb();
···101102 } else actorHandle = actor;
102103103104 // TODO: Add placeholder avatar
105105+ // This is a mess and should be replaced with a real solution!
104106 const avatarCid: string =
105107 profileSearch[0]?.avatar || "https://missing.avatar";
106108 let actorAvatar;
107109 if (avatarCid !== "https://missing.avatar") {
108108- actorAvatar = await getUriFromBlobCid(actorDid, avatarCid);
110110+ actorAvatar = await createAvatarLink(actorDid, avatarCid);
109111 } else actorAvatar = avatarCid;
110112111113 // Right now we don't do de-duplication in the database, so we just take the
···118120 description: profileSearch[0]?.description || null,
119121 createdAt: profileSearch[0]?.createdAt,
120122 });
123123+});
124124+125125+app.get("/social.clippr.feed.getClips", async (c) => {
126126+ const uris = c.req.query("uris");
127127+ if (uris === undefined || uris.trim().length === 0) {
128128+ return c.json(
129129+ {
130130+ error: "InvalidRequest",
131131+ message: "Error: Parameters must have the uris property included",
132132+ },
133133+ 400,
134134+ );
135135+ }
136136+137137+ const rawUriArray: string[] = uris.split(",");
138138+139139+ if (rawUriArray.length > 25) {
140140+ return c.json(
141141+ {
142142+ error: "InvalidRequest",
143143+ message: "Error: More than 25 URIs have been provided",
144144+ },
145145+ 400,
146146+ );
147147+ }
148148+149149+ if (
150150+ rawUriArray.some((value) => {
151151+ return !value.startsWith("at://");
152152+ })
153153+ ) {
154154+ return c.json(
155155+ {
156156+ error: "InvalidRequest",
157157+ message: "Error: A queried URI is missing the at:// identifier",
158158+ },
159159+ 400,
160160+ );
161161+ }
162162+163163+ const parsedUriArray: object[] = [];
164164+165165+ for (let value of rawUriArray) {
166166+ value = value.replace("at://", "");
167167+ const splitUri: string[] = value.split("/");
168168+169169+ if (splitUri.length !== 3) {
170170+ c.json(
171171+ {
172172+ error: "InvalidRequest",
173173+ message: "Error: A queried URI is not a proper clip",
174174+ },
175175+ 400,
176176+ );
177177+ }
178178+179179+ let splitUriObject: ClipViewQuery = {
180180+ did: "",
181181+ collection: "",
182182+ recordKey: "",
183183+ };
184184+185185+ // validate type
186186+ if (
187187+ !splitUri[0] ||
188188+ !splitUri[1] ||
189189+ !splitUri[2] ||
190190+ typeof splitUri[0] !== "string" ||
191191+ typeof splitUri[1] !== "string" ||
192192+ typeof splitUri[2] !== "string"
193193+ ) {
194194+ c.json(
195195+ {
196196+ error: "InvalidRequest",
197197+ message: "Error: A queried URI is not a proper clip",
198198+ },
199199+ 400,
200200+ );
201201+ } else {
202202+ splitUriObject = {
203203+ did: splitUri[0],
204204+ collection: splitUri[1],
205205+ recordKey: splitUri[2],
206206+ };
207207+ }
208208+209209+ const clipView = await createClipView(splitUriObject);
210210+211211+ if (!is(SocialClipprFeedDefs.clipViewSchema, value)) {
212212+ c.json(clipView, 400);
213213+ }
214214+215215+ parsedUriArray.push(clipView);
216216+ }
217217+218218+ if (parsedUriArray.length === 0) {
219219+ return c.json(
220220+ {
221221+ error: "InvalidRequest",
222222+ message: "No queried URIs exist",
223223+ } as ErrorResponse,
224224+ 400,
225225+ );
226226+ }
227227+228228+ return c.json(parsedUriArray, 200);
121229});
122230123231app.get("/_health", async (c) => {
+1226-1290
backend/static/api.json
···11{
22- "openapi": "3.1.1",
33- "info": {
44- "title": "Clippr AppView API",
55- "version": "1.0.1",
66- "description": "API reference documentation for Clippr's backend.",
77- "license": {
88- "name": "GNU Affero General Public License v3.0 only",
99- "identifier": "AGPL-3.0-only"
1010- }
1111- },
1212- "servers": [
1313- {
1414- "url": "http://localhost:9090",
1515- "description": "Development server"
1616- },
1717- {
1818- "url": "https://api.clippr.social",
1919- "description": "Production server"
2020- }
2121- ],
2222- "tags": [
2323- {
2424- "name": "Clips",
2525- "description": "API paths that relate to user bookmarks, or 'clips'."
2626- },
2727- {
2828- "name": "Tags",
2929- "description": "API paths that relate to user tags."
3030- },
3131- {
3232- "name": "Profile",
3333- "description": "API paths that relate to user profiles."
3434- },
3535- {
3636- "name": "Misc",
3737- "description": "API paths that don't fit into any other category."
3838- }
3939- ],
4040- "paths": {
4141- "/xrpc/social.clippr.actor.getPreferences": {
4242- "get": {
4343- "tags": [
4444- "Profile"
4545- ],
4646- "summary": "Get a user's preferences",
4747- "operationId": "social.clippr.actor.getPreferences",
4848- "description": "Get a user's private preferences. Requires authentication.",
4949- "security": [
5050- {
5151- "Bearer": []
5252- }
5353- ],
5454- "responses": {
5555- "200": {
5656- "description": "OK",
5757- "content": {
5858- "application/json": {
5959- "schema": {
6060- "$ref": "#/components/schemas/social.clippr.actor.defs.preferences"
6161- }
6262- }
6363- }
6464- },
6565- "400": {
6666- "description": "Bad Request",
6767- "content": {
6868- "application/json": {
6969- "schema": {
7070- "type": "object",
7171- "properties": {
7272- "error": {
7373- "type": "string",
7474- "description": "A general error code",
7575- "oneOf": [
7676- {
7777- "const": "InvalidRequest"
7878- },
7979- {
8080- "const": "ExpiredToken"
8181- },
8282- {
8383- "const": "InvalidToken"
8484- }
8585- ]
8686- },
8787- "message": {
8888- "type": "string",
8989- "description": "A detailed description of the error"
9090- }
9191- }
9292- }
9393- }
9494- }
9595- },
9696- "401": {
9797- "description": "Unauthorized",
9898- "content": {
9999- "application/json": {
100100- "schema": {
101101- "type": "object",
102102- "properties": {
103103- "error": {
104104- "type": "string",
105105- "description": "A general error code",
106106- "oneOf": [
107107- {
108108- "const": "AuthMissing"
109109- }
110110- ]
111111- },
112112- "message": {
113113- "type": "string",
114114- "description": "A detailed description of the error"
115115- }
116116- }
117117- }
118118- }
119119- }
120120- }
121121- }
122122- }
123123- },
124124- "/xrpc/social.clippr.actor.getProfile": {
125125- "get": {
126126- "tags": [
127127- "Profile"
128128- ],
129129- "summary": "Get a profile",
130130- "operationId": "social.clippr.actor.getProfile",
131131- "description": "Get a user's profile based on a given DID or handle.",
132132- "parameters": [
133133- {
134134- "name": "actor",
135135- "in": "query",
136136- "description": "Handle or DID of account to fetch profile of",
137137- "required": true,
138138- "content": {
139139- "schema": {
140140- "type": "string",
141141- "description": "Handle or DID of account to fetch profile of",
142142- "format": "at-identifier"
143143- }
144144- },
145145- "deprecated": false,
146146- "allowEmptyValue": false
147147- }
148148- ],
149149- "responses": {
150150- "200": {
151151- "description": "OK",
152152- "content": {
153153- "application/json": {
154154- "schema": {
155155- "type": "object",
156156- "$ref": "#/components/schemas/social.clippr.actor.defs.profileView"
157157- }
158158- }
159159- }
160160- },
161161- "400": {
162162- "description": "Bad Request",
163163- "content": {
164164- "application/json": {
165165- "schema": {
166166- "type": "object",
167167- "properties": {
168168- "error": {
169169- "type": "string",
170170- "description": "A general error code",
171171- "oneOf": [
172172- {
173173- "const": "InvalidRequest"
174174- }
175175- ]
176176- },
177177- "message": {
178178- "type": "string",
179179- "description": "A detailed description of the error"
180180- }
181181- }
182182- }
183183- }
184184- }
185185- }
186186- }
187187- }
188188- },
189189- "/xrpc/social.clippr.actor.putPreferences": {
190190- "post": {
191191- "tags": [
192192- "Profile"
193193- ],
194194- "summary": "Set a user's preferences",
195195- "operationId": "social.clippr.actor.putPreferences",
196196- "description": "Sets the private preferences attached to the account. Requires authentication.",
197197- "security": [
198198- {
199199- "Bearer": []
200200- }
201201- ],
202202- "requestBody": {
203203- "required": true,
204204- "content": {
205205- "application/json": {
206206- "schema": {
207207- "type": "object",
208208- "properties": {
209209- "preferences": {
210210- "$ref": "#/components/schemas/social.clippr.actor.defs.preferences"
211211- }
212212- }
213213- }
214214- }
215215- }
216216- },
217217- "responses": {
218218- "200": {
219219- "description": "OK"
220220- },
221221- "400": {
222222- "description": "Bad Request",
223223- "content": {
224224- "application/json": {
225225- "schema": {
226226- "type": "object",
227227- "properties": {
228228- "error": {
229229- "type": "string",
230230- "oneOf": [
231231- {
232232- "const": "InvalidRequest"
233233- },
234234- {
235235- "const": "ExpiredToken"
236236- },
237237- {
238238- "const": "InvalidToken"
239239- }
240240- ],
241241- "description": "A general error code"
242242- },
243243- "message": {
244244- "type": "string",
245245- "description": "A detailed description of the error"
246246- }
247247- }
248248- }
249249- }
250250- }
251251- },
252252- "401": {
253253- "description": "Unauthorized",
254254- "content": {
255255- "application/json": {
256256- "schema": {
257257- "type": "object",
258258- "properties": {
259259- "error": {
260260- "type": "string",
261261- "description": "A general error code",
262262- "oneOf": [
263263- {
264264- "const": "AuthMissing"
265265- }
266266- ]
267267- },
268268- "message": {
269269- "type": "string",
270270- "description": "A detailed description of the error"
271271- }
272272- }
273273- }
274274- }
275275- }
276276- }
277277- }
278278- }
279279- },
280280- "/xrpc/social.clippr.actor.searchClips": {
281281- "get": {
282282- "tags": [
283283- "Clips"
284284- ],
285285- "summary": "Search clips",
286286- "operationId": "social.clippr.actor.searchClips",
287287- "description": "Find clips matching search criteria.",
288288- "parameters": [
289289- {
290290- "name": "q",
291291- "in": "query",
292292- "description": "Search query string",
293293- "required": true,
294294- "schema": {
295295- "type": "string",
296296- "description": "Search query string"
297297- }
298298- },
299299- {
300300- "name": "limit",
301301- "in": "query",
302302- "description": "How many clips to return in the query output",
303303- "required": false,
304304- "schema": {
305305- "type": "integer",
306306- "minimum": 1,
307307- "maximum": 100,
308308- "default": 25
309309- }
310310- },
311311- {
312312- "name": "actor",
313313- "in": "query",
314314- "description": "An actor to filter results to",
315315- "required": false,
316316- "schema": {
317317- "type": "string",
318318- "description": "An actor to filter results to",
319319- "format": "at-identifier"
320320- }
321321- },
322322- {
323323- "name": "cursor",
324324- "in": "query",
325325- "description": "A parameter to paginate results",
326326- "required": false,
327327- "schema": {
328328- "type": "string",
329329- "description": "A parameter to paginate results"
330330- }
331331- }
332332- ],
333333- "responses": {
334334- "200": {
335335- "description": "OK",
336336- "content": {
337337- "application/json": {
338338- "schema": {
339339- "type": "object",
340340- "properties": {
341341- "cursor": {
342342- "type": "string",
343343- "description": "A parameter to paginate results"
344344- },
345345- "clips": {
346346- "type": "array",
347347- "items": {
348348- "$ref": "#/components/schemas/social.clippr.feed.defs.clipView"
349349- }
350350- }
351351- }
352352- }
353353- }
354354- }
355355- },
356356- "400": {
357357- "description": "Bad Request",
358358- "content": {
359359- "application/json": {
360360- "schema": {
361361- "type": "object",
362362- "properties": {
363363- "error": {
364364- "type": "string",
365365- "description": "A general error code",
366366- "oneOf": [
367367- {
368368- "const": "InvalidRequest"
369369- }
370370- ]
371371- },
372372- "message": {
373373- "type": "string",
374374- "description": "A detailed description of the error"
375375- }
376376- }
377377- }
378378- }
379379- }
380380- }
381381- }
382382- }
383383- },
384384- "/xrpc/social.clippr.actor.searchProfiles": {
385385- "get": {
386386- "tags": [
387387- "Profile"
388388- ],
389389- "summary": "Search profiles",
390390- "operationId": "social.clippr.actor.searchProfiles",
391391- "description": "Find profiles matching search criteria.",
392392- "parameters": [
393393- {
394394- "name": "q",
395395- "in": "query",
396396- "description": "Search query string",
397397- "required": false,
398398- "schema": {
399399- "type": "string",
400400- "description": "Search query string"
401401- }
402402- },
403403- {
404404- "name": "limit",
405405- "in": "query",
406406- "description": "The number of profiles to be returned in the query",
407407- "required": false,
408408- "schema": {
409409- "type": "integer",
410410- "minimum": 1,
411411- "maximum": 100,
412412- "default": 25
413413- }
414414- },
415415- {
416416- "name": "cursor",
417417- "in": "query",
418418- "description": "A parameter used for pagination",
419419- "required": false,
420420- "schema": {
421421- "type": "string",
422422- "description": "A parameter used for pagination"
423423- }
424424- }
425425- ],
426426- "responses": {
427427- "200": {
428428- "description": "OK",
429429- "content": {
430430- "application/json": {
431431- "schema": {
432432- "type": "object",
433433- "properties": {
434434- "cursor": {
435435- "type": "string",
436436- "description": "A parameter used for pagination"
437437- },
438438- "actors": {
439439- "type": "array",
440440- "items": {
441441- "$ref": "#/components/schemas/social.clippr.actor.defs.profileView"
442442- }
443443- }
444444- }
445445- }
446446- }
447447- }
448448- },
449449- "400": {
450450- "description": "Bad Request",
451451- "content": {
452452- "application/json": {
453453- "schema": {
454454- "type": "object",
455455- "properties": {
456456- "error": {
457457- "type": "string",
458458- "description": "A general error code",
459459- "oneOf": [
460460- {
461461- "const": "InvalidRequest"
462462- }
463463- ]
464464- },
465465- "message": {
466466- "type": "string",
467467- "description": "A detailed description of the error"
468468- }
469469- }
470470- }
471471- }
472472- }
473473- }
474474- }
475475- }
476476- },
477477- "/xrpc/social.clippr.actor.searchTags": {
478478- "get": {
479479- "tags": [
480480- "Tags"
481481- ],
482482- "summary": "Search tags",
483483- "operationId": "social.clippr.actor.searchTags",
484484- "description": "Find tags matching search criteria.",
485485- "parameters": [
486486- {
487487- "name": "q",
488488- "in": "query",
489489- "description": "Search query string",
490490- "required": true,
491491- "schema": {
492492- "type": "string",
493493- "description": "Search query string"
494494- }
495495- },
496496- {
497497- "name": "limit",
498498- "in": "query",
499499- "description": "How many tags to return in the query output",
500500- "required": false,
501501- "schema": {
502502- "type": "integer",
503503- "minimum": 1,
504504- "maximum": 100,
505505- "default": 25
506506- }
507507- },
508508- {
509509- "name": "actor",
510510- "in": "query",
511511- "description": "An actor to filter results to",
512512- "required": false,
513513- "schema": {
514514- "type": "string",
515515- "description": "An actor to filter results to",
516516- "format": "at-identifier"
517517- }
518518- },
519519- {
520520- "name": "cursor",
521521- "in": "query",
522522- "description": "A parameter to paginate results",
523523- "required": false,
524524- "schema": {
525525- "type": "string",
526526- "description": "A parameter to paginate results"
527527- }
528528- }
529529- ],
530530- "responses": {
531531- "200": {
532532- "description": "OK",
533533- "content": {
534534- "application/json": {
535535- "schema": {
536536- "type": "object",
537537- "properties": {
538538- "cursor": {
539539- "type": "string",
540540- "description": "A parameter to paginate results"
541541- },
542542- "tags": {
543543- "type": "array",
544544- "items": {
545545- "$ref": "#/components/schemas/social.clippr.feed.defs.tagView"
546546- }
547547- }
548548- }
549549- }
550550- }
551551- }
552552- },
553553- "400": {
554554- "description": "Bad Request",
555555- "content": {
556556- "application/json": {
557557- "schema": {
558558- "type": "object",
559559- "properties": {
560560- "error": {
561561- "type": "string",
562562- "description": "A general error code",
563563- "oneOf": [
564564- {
565565- "const": "InvalidRequest"
566566- }
567567- ]
568568- },
569569- "message": {
570570- "type": "string",
571571- "description": "A detailed description of the error"
572572- }
573573- }
574574- }
575575- }
576576- }
577577- }
578578- }
579579- }
580580- },
581581- "/xrpc/social.clippr.feed.getClips": {
582582- "get": {
583583- "tags": [
584584- "Clips"
585585- ],
586586- "summary": "Get clips",
587587- "operationId": "social.clippr.feed.getClips",
588588- "description": "Get the hydrated views of a list of clips from their AT URIs.",
589589- "parameters": [
590590- {
591591- "name": "uris",
592592- "in": "query",
593593- "description": "List of tag AT-URIs to return hydrated views for",
594594- "required": true,
595595- "schema": {
596596- "type": "array",
597597- "items": {
598598- "type": "string",
599599- "format": "at-uri"
600600- },
601601- "maxItems": 25
602602- }
603603- }
604604- ],
605605- "responses": {
606606- "200": {
607607- "description": "OK",
608608- "content": {
609609- "application/json": {
610610- "schema": {
611611- "type": "array",
612612- "items": {
613613- "$ref": "#/components/schemas/social.clippr.feed.defs.clipView"
614614- }
615615- }
616616- }
617617- }
618618- },
619619- "400": {
620620- "description": "Bad Request",
621621- "content": {
622622- "application/json": {
623623- "schema": {
624624- "type": "object",
625625- "properties": {
626626- "error": {
627627- "type": "string",
628628- "description": "A general error code",
629629- "oneOf": [
630630- {
631631- "const": "InvalidRequest"
632632- }
633633- ]
634634- },
635635- "message": {
636636- "type": "string",
637637- "description": "A detailed description of the error"
638638- }
639639- }
640640- }
641641- }
642642- }
643643- }
644644- }
645645- }
646646- },
647647- "/xrpc/social.clippr.feed.getTags": {
648648- "get": {
649649- "tags": [
650650- "Tags"
651651- ],
652652- "summary": "Get tags",
653653- "operationId": "social.clippr.feed.getTags",
654654- "description": "Get a the hydrated views of a list of tags from their AT URIs.",
655655- "parameters": [
656656- {
657657- "name": "uris",
658658- "in": "query",
659659- "description": "List of tag AT-URIs to return hydrated views for",
660660- "required": true,
661661- "schema": {
662662- "type": "array",
663663- "items": {
664664- "type": "string",
665665- "format": "at-uri"
666666- },
667667- "maxItems": 25
668668- }
669669- }
670670- ],
671671- "responses": {
672672- "200": {
673673- "description": "OK",
674674- "content": {
675675- "application/json": {
676676- "schema": {
677677- "type": "array",
678678- "items": {
679679- "$ref": "#/components/schemas/social.clippr.feed.defs.tagView"
680680- }
681681- }
682682- }
683683- }
684684- },
685685- "400": {
686686- "description": "Bad Request",
687687- "content": {
688688- "application/json": {
689689- "schema": {
690690- "type": "object",
691691- "properties": {
692692- "error": {
693693- "type": "string",
694694- "description": "A general error code",
695695- "oneOf": [
696696- {
697697- "const": "InvalidRequest"
698698- }
699699- ]
700700- },
701701- "message": {
702702- "type": "string",
703703- "description": "A detailed description of the error"
704704- }
705705- }
706706- }
707707- }
708708- }
709709- }
710710- }
711711- }
712712- },
713713- "/xrpc/social.clippr.feed.getProfileClips": {
714714- "get": {
715715- "tags": [
716716- "Clips"
717717- ],
718718- "summary": "Get a profile's clip feed",
719719- "operationId": "social.clippr.feed.getProfileClips",
720720- "description": "Get a view of a profile's reverse-chronological clips feed.",
721721- "parameters": [
722722- {
723723- "name": "actor",
724724- "in": "query",
725725- "description": "An actor to get feed data from",
726726- "required": true,
727727- "schema": {
728728- "type": "string",
729729- "description": "An actor to get feed data from",
730730- "format": "at-identifier"
731731- }
732732- },
733733- {
734734- "name": "limit",
735735- "in": "query",
736736- "description": "How many results to return with the query",
737737- "required": false,
738738- "schema": {
739739- "type": "integer",
740740- "minimum": 1,
741741- "maximum": 100,
742742- "default": 50
743743- }
744744- },
745745- {
746746- "name": "cursor",
747747- "in": "query",
748748- "description": "A parameter to paginate results",
749749- "required": false,
750750- "schema": {
751751- "type": "string",
752752- "description": "A parameter to paginate results"
753753- }
754754- },
755755- {
756756- "name": "filter",
757757- "in": "query",
758758- "description": "What types to include in response",
759759- "required": false,
760760- "schema": {
761761- "type": "string",
762762- "description": "What types of clips to include in response",
763763- "default": "all_clips",
764764- "enum": [
765765- "all_clips",
766766- "tagged_clips",
767767- "untagged_clips"
768768- ]
769769- }
770770- }
771771- ],
772772- "responses": {
773773- "200": {
774774- "description": "OK",
775775- "content": {
776776- "application/json": {
777777- "schema": {
778778- "type": "object",
779779- "properties": {
780780- "cursor": {
781781- "type": "string"
782782- },
783783- "feed": {
784784- "type": "array",
785785- "items": {
786786- "$ref": "#/components/schemas/social.clippr.feed.defs.clipView"
787787- }
788788- }
789789- }
790790- }
791791- }
792792- }
793793- },
794794- "400": {
795795- "description": "Bad Request",
796796- "content": {
797797- "application/json": {
798798- "schema": {
799799- "type": "object",
800800- "properties": {
801801- "error": {
802802- "type": "string",
803803- "description": "A general error code",
804804- "oneOf": [
805805- {
806806- "const": "InvalidRequest"
807807- }
808808- ]
809809- },
810810- "message": {
811811- "type": "string",
812812- "description": "A detailed description of the error"
813813- }
814814- }
815815- }
816816- }
817817- }
818818- }
819819- }
820820- }
821821- },
822822- "/xrpc/social.clippr.feed.getProfileTags": {
823823- "get": {
824824- "tags": [
825825- "Tags"
826826- ],
827827- "summary": "Get a profile's tag feed",
828828- "operationId": "social.clippr.feed.getProfileTags",
829829- "description": "Get a view of a profile's reverse-chronological clips feed.",
830830- "parameters": [
831831- {
832832- "name": "actor",
833833- "in": "query",
834834- "description": "An actor to get feed data from",
835835- "required": true,
836836- "schema": {
837837- "type": "string",
838838- "description": "An actor to get feed data from",
839839- "format": "at-identifier"
840840- }
841841- },
842842- {
843843- "name": "limit",
844844- "in": "query",
845845- "description": "How many results to return with the query",
846846- "required": false,
847847- "schema": {
848848- "type": "integer",
849849- "minimum": 1,
850850- "maximum": 100,
851851- "default": 50
852852- }
853853- },
854854- {
855855- "name": "cursor",
856856- "in": "query",
857857- "description": "A parameter to paginate results",
858858- "required": false,
859859- "schema": {
860860- "type": "string",
861861- "description": "A parameter to paginate results"
862862- }
863863- }
864864- ],
865865- "responses": {
866866- "200": {
867867- "description": "OK",
868868- "content": {
869869- "application/json": {
870870- "schema": {
871871- "type": "object",
872872- "properties": {
873873- "cursor": {
874874- "type": "string"
875875- },
876876- "feed": {
877877- "type": "array",
878878- "items": {
879879- "$ref": "#/components/schemas/social.clippr.feed.defs.tagView"
880880- }
881881- }
882882- }
883883- }
884884- }
885885- }
886886- },
887887- "400": {
888888- "description": "Bad Request",
889889- "content": {
890890- "application/json": {
891891- "schema": {
892892- "type": "object",
893893- "properties": {
894894- "error": {
895895- "type": "string",
896896- "description": "A general error code",
897897- "oneOf": [
898898- {
899899- "const": "InvalidRequest"
900900- }
901901- ]
902902- },
903903- "message": {
904904- "type": "string",
905905- "description": "A detailed description of the error"
906906- }
907907- }
908908- }
909909- }
910910- }
911911- }
912912- }
913913- }
914914- },
915915- "/xrpc/social.clippr.feed.getTagList": {
916916- "get": {
917917- "tags": [
918918- "Tags"
919919- ],
920920- "summary": "Get a profile's tag list",
921921- "operationId": "social.clippr.feed.getProfileTags",
922922- "description": "Get a profile's complete list of tags.",
923923- "parameters": [
924924- {
925925- "name": "actor",
926926- "in": "query",
927927- "description": "An actor to fetch the tag list from",
928928- "required": false,
929929- "schema": {
930930- "type": "string",
931931- "description": "An actor to fetch the tag list from",
932932- "format": "at-identifier"
933933- }
934934- }
935935- ],
936936- "responses": {
937937- "200": {
938938- "description": "OK",
939939- "content": {
940940- "application/json": {
941941- "schema": {
942942- "type": "object",
943943- "properties": {
944944- "tags": {
945945- "type": "array",
946946- "items": {
947947- "$ref": "#/components/schemas/social.clippr.feed.defs.tagView"
948948- }
949949- }
950950- }
951951- }
952952- }
953953- }
954954- },
955955- "400": {
956956- "description": "Bad Request",
957957- "content": {
958958- "application/json": {
959959- "schema": {
960960- "type": "object",
961961- "properties": {
962962- "error": {
963963- "type": "string",
964964- "description": "A general error code",
965965- "oneOf": [
966966- {
967967- "error": "InvalidRequest"
968968- }
969969- ]
970970- },
971971- "message": {
972972- "type": "string",
973973- "description": "A detailed description of the error"
974974- }
975975- }
976976- }
977977- }
978978- }
979979- }
980980- }
981981- }
982982- },
983983- "/xrpc/_health": {
984984- "get": {
985985- "summary": "Health check",
986986- "description": "Check the health of the server. If it is functioning properly, you will receive the server's version number.",
987987- "responses": {
988988- "200": {
989989- "description": "OK",
990990- "content": {
991991- "application/json": {
992992- "schema": {
993993- "type": "object",
994994- "properties": {
995995- "version": {
996996- "type": "string",
997997- "description": "The version number of the AppView."
998998- }
999999- }
10001000- }
10011001- }
10021002- }
10031003- }
10041004- },
10051005- "tags": [
10061006- "Misc"
10071007- ]
10081008- }
10091009- }
10101010- },
10111011- "components": {
10121012- "schemas": {
10131013- "com.atproto.repo.strongRef": {
10141014- "type": "object",
10151015- "required": [
10161016- "uri",
10171017- "cid"
10181018- ],
10191019- "properties": {
10201020- "uri": {
10211021- "type": "string",
10221022- "format": "at-uri"
10231023- },
10241024- "cid": {
10251025- "type": "string",
10261026- "format": "cid"
10271027- }
10281028- }
10291029- },
10301030- "social.clippr.actor.defs.profileView": {
10311031- "type": "object",
10321032- "description": "A view of an actor's profile",
10331033- "required": [
10341034- "did",
10351035- "handle",
10361036- "displayName"
10371037- ],
10381038- "properties": {
10391039- "did": {
10401040- "type": "string",
10411041- "description": "The DID of the profile",
10421042- "format": "did"
10431043- },
10441044- "handle": {
10451045- "type": "string",
10461046- "description": "The handle of the profile",
10471047- "format": "handle"
10481048- },
10491049- "displayName": {
10501050- "type": "string",
10511051- "description": "The display name associated to the profile",
10521052- "maxLength": 64
10531053- },
10541054- "description": {
10551055- "type": "string",
10561056- "description": "The biography associated to the profile",
10571057- "maxLength": 500
10581058- },
10591059- "avatar": {
10601060- "type": "string",
10611061- "description": "A link to the profile's avatar",
10621062- "format": "uri"
10631063- },
10641064- "createdAt": {
10651065- "type": "string",
10661066- "description": "When the profile record was first created",
10671067- "format": "date-time"
10681068- }
10691069- }
10701070- },
10711071- "social.clippr.actor.defs.preferences": {
10721072- "type": "array",
10731073- "items": {
10741074- "oneOf": [
10751075- {
10761076- "$ref": "#/components/schemas/social.clippr.actor.defs.publishingScopesPref"
10771077- }
10781078- ]
10791079- }
10801080- },
10811081- "social.clippr.actor.defs.publishingScopesPref": {
10821082- "type": "object",
10831083- "description": "Preferences for a user's publishing scopes",
10841084- "required": [
10851085- "defaultScope"
10861086- ],
10871087- "properties": {
10881088- "defaultScope": {
10891089- "type": "string",
10901090- "description": "What publishing scope to mark a clip as by default",
10911091- "enum": [
10921092- "public",
10931093- "unlisted"
10941094- ]
10951095- }
10961096- }
10971097- },
10981098- "social.clippr.feed.defs.clipView": {
10991099- "type": "object",
11001100- "description": "A view of a single bookmark (or 'clip')",
11011101- "required": [
11021102- "uri",
11031103- "cid",
11041104- "author",
11051105- "record",
11061106- "indexedAt"
11071107- ],
11081108- "properties": {
11091109- "uri": {
11101110- "type": "string",
11111111- "description": "The AT-URI of the clip",
11121112- "format": "at-uri"
11131113- },
11141114- "cid": {
11151115- "type": "string",
11161116- "description": "The CID of the clip",
11171117- "format": "cid"
11181118- },
11191119- "author": {
11201120- "description": "A reference to the actor's profile",
11211121- "$ref": "#/components/schemas/social.clippr.actor.defs.profileView"
11221122- },
11231123- "record": {
11241124- "type": "object",
11251125- "description": "The raw record of the clip"
11261126- },
11271127- "indexedAt": {
11281128- "type": "string",
11291129- "description": "The time in which the clip's record was indexed by the AppView",
11301130- "format": "date-time"
11311131- }
11321132- }
11331133- },
11341134- "social.clippr.feed.defs.tagView": {
11351135- "type": "object",
11361136- "description": "A view of a single tag",
11371137- "required": [
11381138- "uri",
11391139- "cid",
11401140- "author",
11411141- "record",
11421142- "indexedAt"
11431143- ],
11441144- "properties": {
11451145- "uri": {
11461146- "type": "string",
11471147- "description": "The AT-URI to the tag",
11481148- "format": "at-uri"
11491149- },
11501150- "cid": {
11511151- "type": "string",
11521152- "description": "The CID of the tag",
11531153- "format": "cid"
11541154- },
11551155- "author": {
11561156- "description": "A reference to the actor's profile",
11571157- "$ref": "#/components/schemas/social.clippr.actor.defs.profileView"
11581158- },
11591159- "record": {
11601160- "type": "object",
11611161- "description": "The raw record of the clip"
11621162- },
11631163- "indexedAt": {
11641164- "type": "string",
11651165- "description": "The time in which the tag's record was indexed by the AppView",
11661166- "format": "date-time"
11671167- }
11681168- }
11691169- },
11701170- "social.clippr.actor.profile": {
11711171- "type": "object",
11721172- "required": [
11731173- "createdAt",
11741174- "displayName"
11751175- ],
11761176- "properties": {
11771177- "displayName": {
11781178- "type": "string",
11791179- "description": "A display name to be shown on a profile",
11801180- "maxLength": 64
11811181- },
11821182- "description": {
11831183- "type": "string",
11841184- "description": "Text for user to describe themselves",
11851185- "maxLength": 500
11861186- },
11871187- "avatar": {
11881188- "type": "blob",
11891189- "maxSize": 1000000,
11901190- "description": "Image to show on user's profiles"
11911191- },
11921192- "createdAt": {
11931193- "type": "string",
11941194- "description": "The creation date of the profile",
11951195- "format": "date-time"
11961196- }
11971197- }
11981198- },
11991199- "social.clippr.feed.clip": {
12001200- "type": "object",
12011201- "required": [
12021202- "url",
12031203- "title",
12041204- "description",
12051205- "unlisted",
12061206- "createdAt"
12071207- ],
12081208- "properties": {
12091209- "url": {
12101210- "type": "string",
12111211- "description": "The URL of the bookmark. Cannot be left empty or be modified after creation.",
12121212- "format": "uri",
12131213- "maxLength": 2000
12141214- },
12151215- "title": {
12161216- "type": "string",
12171217- "description": "The title of the bookmark. If left empty, reuse the URL.",
12181218- "maxLength": 2048
12191219- },
12201220- "description": {
12211221- "type": "string",
12221222- "description": "A description of the bookmark's content. This should be ripped from the URL metadata and be static for all records using the URL.",
12231223- "maxLength": 4096
12241224- },
12251225- "notes": {
12261226- "type": "string",
12271227- "description": "User-written notes for the bookmark. Public and personal.",
12281228- "maxLength": 10000
12291229- },
12301230- "tags": {
12311231- "type": "array",
12321232- "description": "An array of tags. A format of solely alphanumeric characters and dashes should be used.",
12331233- "items": {
12341234- "$ref": "#/components/schemas/com.atproto.repo.strongRef"
12351235- }
12361236- },
12371237- "unlisted": {
12381238- "type": "boolean",
12391239- "description": "Whether the bookmark can be used for feed indexing and aggregation"
12401240- },
12411241- "unread": {
12421242- "type": "boolean",
12431243- "description": "Whether the bookmark has been read by the user",
12441244- "default": true
12451245- },
12461246- "languages": {
12471247- "type": "array",
12481248- "items": {
12491249- "type": "string",
12501250- "format": "language"
12511251- },
12521252- "maxItems": 5
12531253- },
12541254- "createdAt": {
12551255- "type": "string",
12561256- "description": "Client-declared timestamp when the bookmark is created",
12571257- "format": "date-time"
12581258- }
12591259- }
12601260- },
12611261- "social.clippr.feed.tag": {
12621262- "type": "object",
12631263- "required": [
12641264- "name",
12651265- "createdAt"
12661266- ],
12671267- "properties": {
12681268- "name": {
12691269- "type": "string",
12701270- "description": "A de-duplicated string containing the name of the tag",
12711271- "maxLength": 64
12721272- },
12731273- "color": {
12741274- "type": "string",
12751275- "description": "A hexadecimal color code",
12761276- "maxLength": 7
12771277- },
12781278- "description": {
12791279- "type": "string",
12801280- "description": "A description of the tag for additional context",
12811281- "maxLength": 5000
12821282- },
12831283- "createdAt": {
12841284- "type": "string",
12851285- "description": "A client-defined timestamp for the creation of the tag",
12861286- "format": "date-time"
12871287- }
12881288- }
12891289- }
12901290- }
12911291- }
22+ "openapi": "3.1.1",
33+ "info": {
44+ "title": "Clippr AppView API",
55+ "version": "1.0.1",
66+ "description": "API reference documentation for Clippr's backend.",
77+ "license": {
88+ "name": "GNU Affero General Public License v3.0 only",
99+ "identifier": "AGPL-3.0-only"
1010+ }
1111+ },
1212+ "servers": [
1313+ {
1414+ "url": "http://localhost:9090",
1515+ "description": "Development server"
1616+ },
1717+ {
1818+ "url": "https://api.clippr.social",
1919+ "description": "Production server"
2020+ }
2121+ ],
2222+ "tags": [
2323+ {
2424+ "name": "Clips",
2525+ "description": "API paths that relate to user bookmarks, or 'clips'."
2626+ },
2727+ {
2828+ "name": "Tags",
2929+ "description": "API paths that relate to user tags."
3030+ },
3131+ {
3232+ "name": "Profile",
3333+ "description": "API paths that relate to user profiles."
3434+ },
3535+ {
3636+ "name": "Misc",
3737+ "description": "API paths that don't fit into any other category."
3838+ }
3939+ ],
4040+ "paths": {
4141+ "/xrpc/social.clippr.actor.getPreferences": {
4242+ "get": {
4343+ "tags": ["Profile"],
4444+ "summary": "Get a user's preferences",
4545+ "operationId": "social.clippr.actor.getPreferences",
4646+ "description": "Get a user's private preferences. Requires authentication.",
4747+ "security": [
4848+ {
4949+ "Bearer": []
5050+ }
5151+ ],
5252+ "responses": {
5353+ "200": {
5454+ "description": "OK",
5555+ "content": {
5656+ "application/json": {
5757+ "schema": {
5858+ "$ref": "#/components/schemas/social.clippr.actor.defs.preferences"
5959+ }
6060+ }
6161+ }
6262+ },
6363+ "400": {
6464+ "description": "Bad Request",
6565+ "content": {
6666+ "application/json": {
6767+ "schema": {
6868+ "type": "object",
6969+ "properties": {
7070+ "error": {
7171+ "type": "string",
7272+ "description": "A general error code",
7373+ "oneOf": [
7474+ {
7575+ "const": "InvalidRequest"
7676+ },
7777+ {
7878+ "const": "ExpiredToken"
7979+ },
8080+ {
8181+ "const": "InvalidToken"
8282+ }
8383+ ]
8484+ },
8585+ "message": {
8686+ "type": "string",
8787+ "description": "A detailed description of the error"
8888+ }
8989+ }
9090+ }
9191+ }
9292+ }
9393+ },
9494+ "401": {
9595+ "description": "Unauthorized",
9696+ "content": {
9797+ "application/json": {
9898+ "schema": {
9999+ "type": "object",
100100+ "properties": {
101101+ "error": {
102102+ "type": "string",
103103+ "description": "A general error code",
104104+ "oneOf": [
105105+ {
106106+ "const": "AuthMissing"
107107+ }
108108+ ]
109109+ },
110110+ "message": {
111111+ "type": "string",
112112+ "description": "A detailed description of the error"
113113+ }
114114+ }
115115+ }
116116+ }
117117+ }
118118+ }
119119+ }
120120+ }
121121+ },
122122+ "/xrpc/social.clippr.actor.getProfile": {
123123+ "get": {
124124+ "tags": ["Profile"],
125125+ "summary": "Get a profile",
126126+ "operationId": "social.clippr.actor.getProfile",
127127+ "description": "Get a user's profile based on a given DID or handle.",
128128+ "parameters": [
129129+ {
130130+ "name": "actor",
131131+ "in": "query",
132132+ "description": "Handle or DID of account to fetch profile of",
133133+ "required": true,
134134+ "content": {
135135+ "schema": {
136136+ "type": "string",
137137+ "description": "Handle or DID of account to fetch profile of",
138138+ "format": "at-identifier"
139139+ }
140140+ },
141141+ "deprecated": false,
142142+ "allowEmptyValue": false
143143+ }
144144+ ],
145145+ "responses": {
146146+ "200": {
147147+ "description": "OK",
148148+ "content": {
149149+ "application/json": {
150150+ "schema": {
151151+ "type": "object",
152152+ "$ref": "#/components/schemas/social.clippr.actor.defs.profileView"
153153+ }
154154+ }
155155+ }
156156+ },
157157+ "400": {
158158+ "description": "Bad Request",
159159+ "content": {
160160+ "application/json": {
161161+ "schema": {
162162+ "type": "object",
163163+ "properties": {
164164+ "error": {
165165+ "type": "string",
166166+ "description": "A general error code",
167167+ "oneOf": [
168168+ {
169169+ "const": "InvalidRequest"
170170+ }
171171+ ]
172172+ },
173173+ "message": {
174174+ "type": "string",
175175+ "description": "A detailed description of the error"
176176+ }
177177+ }
178178+ }
179179+ }
180180+ }
181181+ }
182182+ }
183183+ }
184184+ },
185185+ "/xrpc/social.clippr.actor.putPreferences": {
186186+ "post": {
187187+ "tags": ["Profile"],
188188+ "summary": "Set a user's preferences",
189189+ "operationId": "social.clippr.actor.putPreferences",
190190+ "description": "Sets the private preferences attached to the account. Requires authentication.",
191191+ "security": [
192192+ {
193193+ "Bearer": []
194194+ }
195195+ ],
196196+ "requestBody": {
197197+ "required": true,
198198+ "content": {
199199+ "application/json": {
200200+ "schema": {
201201+ "type": "object",
202202+ "properties": {
203203+ "preferences": {
204204+ "$ref": "#/components/schemas/social.clippr.actor.defs.preferences"
205205+ }
206206+ }
207207+ }
208208+ }
209209+ }
210210+ },
211211+ "responses": {
212212+ "200": {
213213+ "description": "OK"
214214+ },
215215+ "400": {
216216+ "description": "Bad Request",
217217+ "content": {
218218+ "application/json": {
219219+ "schema": {
220220+ "type": "object",
221221+ "properties": {
222222+ "error": {
223223+ "type": "string",
224224+ "oneOf": [
225225+ {
226226+ "const": "InvalidRequest"
227227+ },
228228+ {
229229+ "const": "ExpiredToken"
230230+ },
231231+ {
232232+ "const": "InvalidToken"
233233+ }
234234+ ],
235235+ "description": "A general error code"
236236+ },
237237+ "message": {
238238+ "type": "string",
239239+ "description": "A detailed description of the error"
240240+ }
241241+ }
242242+ }
243243+ }
244244+ }
245245+ },
246246+ "401": {
247247+ "description": "Unauthorized",
248248+ "content": {
249249+ "application/json": {
250250+ "schema": {
251251+ "type": "object",
252252+ "properties": {
253253+ "error": {
254254+ "type": "string",
255255+ "description": "A general error code",
256256+ "oneOf": [
257257+ {
258258+ "const": "AuthMissing"
259259+ }
260260+ ]
261261+ },
262262+ "message": {
263263+ "type": "string",
264264+ "description": "A detailed description of the error"
265265+ }
266266+ }
267267+ }
268268+ }
269269+ }
270270+ }
271271+ }
272272+ }
273273+ },
274274+ "/xrpc/social.clippr.actor.searchClips": {
275275+ "get": {
276276+ "tags": ["Clips"],
277277+ "summary": "Search clips",
278278+ "operationId": "social.clippr.actor.searchClips",
279279+ "description": "Find clips matching search criteria.",
280280+ "parameters": [
281281+ {
282282+ "name": "q",
283283+ "in": "query",
284284+ "description": "Search query string",
285285+ "required": true,
286286+ "schema": {
287287+ "type": "string",
288288+ "description": "Search query string"
289289+ }
290290+ },
291291+ {
292292+ "name": "limit",
293293+ "in": "query",
294294+ "description": "How many clips to return in the query output",
295295+ "required": false,
296296+ "schema": {
297297+ "type": "integer",
298298+ "minimum": 1,
299299+ "maximum": 100,
300300+ "default": 25
301301+ }
302302+ },
303303+ {
304304+ "name": "actor",
305305+ "in": "query",
306306+ "description": "An actor to filter results to",
307307+ "required": false,
308308+ "schema": {
309309+ "type": "string",
310310+ "description": "An actor to filter results to",
311311+ "format": "at-identifier"
312312+ }
313313+ },
314314+ {
315315+ "name": "cursor",
316316+ "in": "query",
317317+ "description": "A parameter to paginate results",
318318+ "required": false,
319319+ "schema": {
320320+ "type": "string",
321321+ "description": "A parameter to paginate results"
322322+ }
323323+ }
324324+ ],
325325+ "responses": {
326326+ "200": {
327327+ "description": "OK",
328328+ "content": {
329329+ "application/json": {
330330+ "schema": {
331331+ "type": "object",
332332+ "properties": {
333333+ "cursor": {
334334+ "type": "string",
335335+ "description": "A parameter to paginate results"
336336+ },
337337+ "clips": {
338338+ "type": "array",
339339+ "items": {
340340+ "$ref": "#/components/schemas/social.clippr.feed.defs.clipView"
341341+ }
342342+ }
343343+ }
344344+ }
345345+ }
346346+ }
347347+ },
348348+ "400": {
349349+ "description": "Bad Request",
350350+ "content": {
351351+ "application/json": {
352352+ "schema": {
353353+ "type": "object",
354354+ "properties": {
355355+ "error": {
356356+ "type": "string",
357357+ "description": "A general error code",
358358+ "oneOf": [
359359+ {
360360+ "const": "InvalidRequest"
361361+ }
362362+ ]
363363+ },
364364+ "message": {
365365+ "type": "string",
366366+ "description": "A detailed description of the error"
367367+ }
368368+ }
369369+ }
370370+ }
371371+ }
372372+ }
373373+ }
374374+ }
375375+ },
376376+ "/xrpc/social.clippr.actor.searchProfiles": {
377377+ "get": {
378378+ "tags": ["Profile"],
379379+ "summary": "Search profiles",
380380+ "operationId": "social.clippr.actor.searchProfiles",
381381+ "description": "Find profiles matching search criteria.",
382382+ "parameters": [
383383+ {
384384+ "name": "q",
385385+ "in": "query",
386386+ "description": "Search query string",
387387+ "required": false,
388388+ "schema": {
389389+ "type": "string",
390390+ "description": "Search query string"
391391+ }
392392+ },
393393+ {
394394+ "name": "limit",
395395+ "in": "query",
396396+ "description": "The number of profiles to be returned in the query",
397397+ "required": false,
398398+ "schema": {
399399+ "type": "integer",
400400+ "minimum": 1,
401401+ "maximum": 100,
402402+ "default": 25
403403+ }
404404+ },
405405+ {
406406+ "name": "cursor",
407407+ "in": "query",
408408+ "description": "A parameter used for pagination",
409409+ "required": false,
410410+ "schema": {
411411+ "type": "string",
412412+ "description": "A parameter used for pagination"
413413+ }
414414+ }
415415+ ],
416416+ "responses": {
417417+ "200": {
418418+ "description": "OK",
419419+ "content": {
420420+ "application/json": {
421421+ "schema": {
422422+ "type": "object",
423423+ "properties": {
424424+ "cursor": {
425425+ "type": "string",
426426+ "description": "A parameter used for pagination"
427427+ },
428428+ "actors": {
429429+ "type": "array",
430430+ "items": {
431431+ "$ref": "#/components/schemas/social.clippr.actor.defs.profileView"
432432+ }
433433+ }
434434+ }
435435+ }
436436+ }
437437+ }
438438+ },
439439+ "400": {
440440+ "description": "Bad Request",
441441+ "content": {
442442+ "application/json": {
443443+ "schema": {
444444+ "type": "object",
445445+ "properties": {
446446+ "error": {
447447+ "type": "string",
448448+ "description": "A general error code",
449449+ "oneOf": [
450450+ {
451451+ "const": "InvalidRequest"
452452+ }
453453+ ]
454454+ },
455455+ "message": {
456456+ "type": "string",
457457+ "description": "A detailed description of the error"
458458+ }
459459+ }
460460+ }
461461+ }
462462+ }
463463+ }
464464+ }
465465+ }
466466+ },
467467+ "/xrpc/social.clippr.actor.searchTags": {
468468+ "get": {
469469+ "tags": ["Tags"],
470470+ "summary": "Search tags",
471471+ "operationId": "social.clippr.actor.searchTags",
472472+ "description": "Find tags matching search criteria.",
473473+ "parameters": [
474474+ {
475475+ "name": "q",
476476+ "in": "query",
477477+ "description": "Search query string",
478478+ "required": true,
479479+ "schema": {
480480+ "type": "string",
481481+ "description": "Search query string"
482482+ }
483483+ },
484484+ {
485485+ "name": "limit",
486486+ "in": "query",
487487+ "description": "How many tags to return in the query output",
488488+ "required": false,
489489+ "schema": {
490490+ "type": "integer",
491491+ "minimum": 1,
492492+ "maximum": 100,
493493+ "default": 25
494494+ }
495495+ },
496496+ {
497497+ "name": "actor",
498498+ "in": "query",
499499+ "description": "An actor to filter results to",
500500+ "required": false,
501501+ "schema": {
502502+ "type": "string",
503503+ "description": "An actor to filter results to",
504504+ "format": "at-identifier"
505505+ }
506506+ },
507507+ {
508508+ "name": "cursor",
509509+ "in": "query",
510510+ "description": "A parameter to paginate results",
511511+ "required": false,
512512+ "schema": {
513513+ "type": "string",
514514+ "description": "A parameter to paginate results"
515515+ }
516516+ }
517517+ ],
518518+ "responses": {
519519+ "200": {
520520+ "description": "OK",
521521+ "content": {
522522+ "application/json": {
523523+ "schema": {
524524+ "type": "object",
525525+ "properties": {
526526+ "cursor": {
527527+ "type": "string",
528528+ "description": "A parameter to paginate results"
529529+ },
530530+ "tags": {
531531+ "type": "array",
532532+ "items": {
533533+ "$ref": "#/components/schemas/social.clippr.feed.defs.tagView"
534534+ }
535535+ }
536536+ }
537537+ }
538538+ }
539539+ }
540540+ },
541541+ "400": {
542542+ "description": "Bad Request",
543543+ "content": {
544544+ "application/json": {
545545+ "schema": {
546546+ "type": "object",
547547+ "properties": {
548548+ "error": {
549549+ "type": "string",
550550+ "description": "A general error code",
551551+ "oneOf": [
552552+ {
553553+ "const": "InvalidRequest"
554554+ }
555555+ ]
556556+ },
557557+ "message": {
558558+ "type": "string",
559559+ "description": "A detailed description of the error"
560560+ }
561561+ }
562562+ }
563563+ }
564564+ }
565565+ }
566566+ }
567567+ }
568568+ },
569569+ "/xrpc/social.clippr.feed.getClips": {
570570+ "get": {
571571+ "tags": ["Clips"],
572572+ "summary": "Get clips",
573573+ "operationId": "social.clippr.feed.getClips",
574574+ "description": "Get the hydrated views of a list of clips from their AT URIs.",
575575+ "parameters": [
576576+ {
577577+ "name": "uris",
578578+ "in": "query",
579579+ "description": "List of tag AT-URIs to return hydrated views for",
580580+ "required": true,
581581+ "schema": {
582582+ "type": "array",
583583+ "items": {
584584+ "type": "string",
585585+ "format": "at-uri"
586586+ },
587587+ "maxItems": 25
588588+ }
589589+ }
590590+ ],
591591+ "responses": {
592592+ "200": {
593593+ "description": "OK",
594594+ "content": {
595595+ "application/json": {
596596+ "schema": {
597597+ "type": "array",
598598+ "items": {
599599+ "$ref": "#/components/schemas/social.clippr.feed.defs.clipView"
600600+ }
601601+ }
602602+ }
603603+ }
604604+ },
605605+ "400": {
606606+ "description": "Bad Request",
607607+ "content": {
608608+ "application/json": {
609609+ "schema": {
610610+ "type": "object",
611611+ "properties": {
612612+ "error": {
613613+ "type": "string",
614614+ "description": "A general error code",
615615+ "oneOf": [
616616+ {
617617+ "const": "InvalidRequest"
618618+ }
619619+ ]
620620+ },
621621+ "message": {
622622+ "type": "string",
623623+ "description": "A detailed description of the error"
624624+ }
625625+ }
626626+ }
627627+ }
628628+ }
629629+ }
630630+ }
631631+ }
632632+ },
633633+ "/xrpc/social.clippr.feed.getTags": {
634634+ "get": {
635635+ "tags": ["Tags"],
636636+ "summary": "Get tags",
637637+ "operationId": "social.clippr.feed.getTags",
638638+ "description": "Get a the hydrated views of a list of tags from their AT URIs.",
639639+ "parameters": [
640640+ {
641641+ "name": "uris",
642642+ "in": "query",
643643+ "description": "List of tag AT-URIs to return hydrated views for",
644644+ "required": true,
645645+ "schema": {
646646+ "type": "array",
647647+ "items": {
648648+ "type": "string",
649649+ "format": "at-uri"
650650+ },
651651+ "maxItems": 25
652652+ }
653653+ }
654654+ ],
655655+ "responses": {
656656+ "200": {
657657+ "description": "OK",
658658+ "content": {
659659+ "application/json": {
660660+ "schema": {
661661+ "type": "array",
662662+ "items": {
663663+ "$ref": "#/components/schemas/social.clippr.feed.defs.tagView"
664664+ }
665665+ }
666666+ }
667667+ }
668668+ },
669669+ "400": {
670670+ "description": "Bad Request",
671671+ "content": {
672672+ "application/json": {
673673+ "schema": {
674674+ "type": "object",
675675+ "properties": {
676676+ "error": {
677677+ "type": "string",
678678+ "description": "A general error code",
679679+ "oneOf": [
680680+ {
681681+ "const": "InvalidRequest"
682682+ }
683683+ ]
684684+ },
685685+ "message": {
686686+ "type": "string",
687687+ "description": "A detailed description of the error"
688688+ }
689689+ }
690690+ }
691691+ }
692692+ }
693693+ }
694694+ }
695695+ }
696696+ },
697697+ "/xrpc/social.clippr.feed.getProfileClips": {
698698+ "get": {
699699+ "tags": ["Clips"],
700700+ "summary": "Get a profile's clip feed",
701701+ "operationId": "social.clippr.feed.getProfileClips",
702702+ "description": "Get a view of a profile's reverse-chronological clips feed.",
703703+ "parameters": [
704704+ {
705705+ "name": "actor",
706706+ "in": "query",
707707+ "description": "An actor to get feed data from",
708708+ "required": true,
709709+ "schema": {
710710+ "type": "string",
711711+ "description": "An actor to get feed data from",
712712+ "format": "at-identifier"
713713+ }
714714+ },
715715+ {
716716+ "name": "limit",
717717+ "in": "query",
718718+ "description": "How many results to return with the query",
719719+ "required": false,
720720+ "schema": {
721721+ "type": "integer",
722722+ "minimum": 1,
723723+ "maximum": 100,
724724+ "default": 50
725725+ }
726726+ },
727727+ {
728728+ "name": "cursor",
729729+ "in": "query",
730730+ "description": "A parameter to paginate results",
731731+ "required": false,
732732+ "schema": {
733733+ "type": "string",
734734+ "description": "A parameter to paginate results"
735735+ }
736736+ },
737737+ {
738738+ "name": "filter",
739739+ "in": "query",
740740+ "description": "What types to include in response",
741741+ "required": false,
742742+ "schema": {
743743+ "type": "string",
744744+ "description": "What types of clips to include in response",
745745+ "default": "all_clips",
746746+ "enum": ["all_clips", "tagged_clips", "untagged_clips"]
747747+ }
748748+ }
749749+ ],
750750+ "responses": {
751751+ "200": {
752752+ "description": "OK",
753753+ "content": {
754754+ "application/json": {
755755+ "schema": {
756756+ "type": "object",
757757+ "properties": {
758758+ "cursor": {
759759+ "type": "string"
760760+ },
761761+ "feed": {
762762+ "type": "array",
763763+ "items": {
764764+ "$ref": "#/components/schemas/social.clippr.feed.defs.clipView"
765765+ }
766766+ }
767767+ }
768768+ }
769769+ }
770770+ }
771771+ },
772772+ "400": {
773773+ "description": "Bad Request",
774774+ "content": {
775775+ "application/json": {
776776+ "schema": {
777777+ "type": "object",
778778+ "properties": {
779779+ "error": {
780780+ "type": "string",
781781+ "description": "A general error code",
782782+ "oneOf": [
783783+ {
784784+ "const": "InvalidRequest"
785785+ }
786786+ ]
787787+ },
788788+ "message": {
789789+ "type": "string",
790790+ "description": "A detailed description of the error"
791791+ }
792792+ }
793793+ }
794794+ }
795795+ }
796796+ }
797797+ }
798798+ }
799799+ },
800800+ "/xrpc/social.clippr.feed.getProfileTags": {
801801+ "get": {
802802+ "tags": ["Tags"],
803803+ "summary": "Get a profile's tag feed",
804804+ "operationId": "social.clippr.feed.getProfileTags",
805805+ "description": "Get a view of a profile's reverse-chronological clips feed.",
806806+ "parameters": [
807807+ {
808808+ "name": "actor",
809809+ "in": "query",
810810+ "description": "An actor to get feed data from",
811811+ "required": true,
812812+ "schema": {
813813+ "type": "string",
814814+ "description": "An actor to get feed data from",
815815+ "format": "at-identifier"
816816+ }
817817+ },
818818+ {
819819+ "name": "limit",
820820+ "in": "query",
821821+ "description": "How many results to return with the query",
822822+ "required": false,
823823+ "schema": {
824824+ "type": "integer",
825825+ "minimum": 1,
826826+ "maximum": 100,
827827+ "default": 50
828828+ }
829829+ },
830830+ {
831831+ "name": "cursor",
832832+ "in": "query",
833833+ "description": "A parameter to paginate results",
834834+ "required": false,
835835+ "schema": {
836836+ "type": "string",
837837+ "description": "A parameter to paginate results"
838838+ }
839839+ }
840840+ ],
841841+ "responses": {
842842+ "200": {
843843+ "description": "OK",
844844+ "content": {
845845+ "application/json": {
846846+ "schema": {
847847+ "type": "object",
848848+ "properties": {
849849+ "cursor": {
850850+ "type": "string"
851851+ },
852852+ "feed": {
853853+ "type": "array",
854854+ "items": {
855855+ "$ref": "#/components/schemas/social.clippr.feed.defs.tagView"
856856+ }
857857+ }
858858+ }
859859+ }
860860+ }
861861+ }
862862+ },
863863+ "400": {
864864+ "description": "Bad Request",
865865+ "content": {
866866+ "application/json": {
867867+ "schema": {
868868+ "type": "object",
869869+ "properties": {
870870+ "error": {
871871+ "type": "string",
872872+ "description": "A general error code",
873873+ "oneOf": [
874874+ {
875875+ "const": "InvalidRequest"
876876+ }
877877+ ]
878878+ },
879879+ "message": {
880880+ "type": "string",
881881+ "description": "A detailed description of the error"
882882+ }
883883+ }
884884+ }
885885+ }
886886+ }
887887+ }
888888+ }
889889+ }
890890+ },
891891+ "/xrpc/social.clippr.feed.getTagList": {
892892+ "get": {
893893+ "tags": ["Tags"],
894894+ "summary": "Get a profile's tag list",
895895+ "operationId": "social.clippr.feed.getProfileTags",
896896+ "description": "Get a profile's complete list of tags.",
897897+ "parameters": [
898898+ {
899899+ "name": "actor",
900900+ "in": "query",
901901+ "description": "An actor to fetch the tag list from",
902902+ "required": false,
903903+ "schema": {
904904+ "type": "string",
905905+ "description": "An actor to fetch the tag list from",
906906+ "format": "at-identifier"
907907+ }
908908+ }
909909+ ],
910910+ "responses": {
911911+ "200": {
912912+ "description": "OK",
913913+ "content": {
914914+ "application/json": {
915915+ "schema": {
916916+ "type": "object",
917917+ "properties": {
918918+ "tags": {
919919+ "type": "array",
920920+ "items": {
921921+ "$ref": "#/components/schemas/social.clippr.feed.defs.tagView"
922922+ }
923923+ }
924924+ }
925925+ }
926926+ }
927927+ }
928928+ },
929929+ "400": {
930930+ "description": "Bad Request",
931931+ "content": {
932932+ "application/json": {
933933+ "schema": {
934934+ "type": "object",
935935+ "properties": {
936936+ "error": {
937937+ "type": "string",
938938+ "description": "A general error code",
939939+ "oneOf": [
940940+ {
941941+ "error": "InvalidRequest"
942942+ }
943943+ ]
944944+ },
945945+ "message": {
946946+ "type": "string",
947947+ "description": "A detailed description of the error"
948948+ }
949949+ }
950950+ }
951951+ }
952952+ }
953953+ }
954954+ }
955955+ }
956956+ },
957957+ "/xrpc/_health": {
958958+ "get": {
959959+ "summary": "Health check",
960960+ "description": "Check the health of the server. If it is functioning properly, you will receive the server's version number.",
961961+ "responses": {
962962+ "200": {
963963+ "description": "OK",
964964+ "content": {
965965+ "application/json": {
966966+ "schema": {
967967+ "type": "object",
968968+ "properties": {
969969+ "version": {
970970+ "type": "string",
971971+ "description": "The version number of the AppView."
972972+ }
973973+ }
974974+ }
975975+ }
976976+ }
977977+ }
978978+ },
979979+ "tags": ["Misc"]
980980+ }
981981+ }
982982+ },
983983+ "components": {
984984+ "schemas": {
985985+ "com.atproto.repo.strongRef": {
986986+ "type": "object",
987987+ "required": ["uri", "cid"],
988988+ "properties": {
989989+ "uri": {
990990+ "type": "string",
991991+ "format": "at-uri"
992992+ },
993993+ "cid": {
994994+ "type": "string",
995995+ "format": "cid"
996996+ }
997997+ }
998998+ },
999999+ "social.clippr.actor.defs.profileView": {
10001000+ "type": "object",
10011001+ "description": "A view of an actor's profile",
10021002+ "required": ["did", "handle", "displayName"],
10031003+ "properties": {
10041004+ "did": {
10051005+ "type": "string",
10061006+ "description": "The DID of the profile",
10071007+ "format": "did"
10081008+ },
10091009+ "handle": {
10101010+ "type": "string",
10111011+ "description": "The handle of the profile",
10121012+ "format": "handle"
10131013+ },
10141014+ "displayName": {
10151015+ "type": "string",
10161016+ "description": "The display name associated to the profile",
10171017+ "maxLength": 64
10181018+ },
10191019+ "description": {
10201020+ "type": "string",
10211021+ "description": "The biography associated to the profile",
10221022+ "maxLength": 500
10231023+ },
10241024+ "avatar": {
10251025+ "type": "string",
10261026+ "description": "A link to the profile's avatar",
10271027+ "format": "uri"
10281028+ },
10291029+ "createdAt": {
10301030+ "type": "string",
10311031+ "description": "When the profile record was first created",
10321032+ "format": "date-time"
10331033+ }
10341034+ }
10351035+ },
10361036+ "social.clippr.actor.defs.preferences": {
10371037+ "type": "array",
10381038+ "items": {
10391039+ "oneOf": [
10401040+ {
10411041+ "$ref": "#/components/schemas/social.clippr.actor.defs.publishingScopesPref"
10421042+ }
10431043+ ]
10441044+ }
10451045+ },
10461046+ "social.clippr.actor.defs.publishingScopesPref": {
10471047+ "type": "object",
10481048+ "description": "Preferences for a user's publishing scopes",
10491049+ "required": ["defaultScope"],
10501050+ "properties": {
10511051+ "defaultScope": {
10521052+ "type": "string",
10531053+ "description": "What publishing scope to mark a clip as by default",
10541054+ "enum": ["public", "unlisted"]
10551055+ }
10561056+ }
10571057+ },
10581058+ "social.clippr.feed.defs.clipView": {
10591059+ "type": "object",
10601060+ "description": "A view of a single bookmark (or 'clip')",
10611061+ "required": ["uri", "cid", "author", "record", "indexedAt"],
10621062+ "properties": {
10631063+ "uri": {
10641064+ "type": "string",
10651065+ "description": "The AT-URI of the clip",
10661066+ "format": "at-uri"
10671067+ },
10681068+ "cid": {
10691069+ "type": "string",
10701070+ "description": "The CID of the clip",
10711071+ "format": "cid"
10721072+ },
10731073+ "author": {
10741074+ "description": "A reference to the actor's profile",
10751075+ "$ref": "#/components/schemas/social.clippr.actor.defs.profileView"
10761076+ },
10771077+ "record": {
10781078+ "type": "object",
10791079+ "description": "The raw record of the clip"
10801080+ },
10811081+ "indexedAt": {
10821082+ "type": "string",
10831083+ "description": "The time in which the clip's record was indexed by the AppView",
10841084+ "format": "date-time"
10851085+ }
10861086+ }
10871087+ },
10881088+ "social.clippr.feed.defs.tagView": {
10891089+ "type": "object",
10901090+ "description": "A view of a single tag",
10911091+ "required": ["uri", "cid", "author", "record", "indexedAt"],
10921092+ "properties": {
10931093+ "uri": {
10941094+ "type": "string",
10951095+ "description": "The AT-URI to the tag",
10961096+ "format": "at-uri"
10971097+ },
10981098+ "cid": {
10991099+ "type": "string",
11001100+ "description": "The CID of the tag",
11011101+ "format": "cid"
11021102+ },
11031103+ "author": {
11041104+ "description": "A reference to the actor's profile",
11051105+ "$ref": "#/components/schemas/social.clippr.actor.defs.profileView"
11061106+ },
11071107+ "record": {
11081108+ "type": "object",
11091109+ "description": "The raw record of the clip"
11101110+ },
11111111+ "indexedAt": {
11121112+ "type": "string",
11131113+ "description": "The time in which the tag's record was indexed by the AppView",
11141114+ "format": "date-time"
11151115+ }
11161116+ }
11171117+ },
11181118+ "social.clippr.actor.profile": {
11191119+ "type": "object",
11201120+ "required": ["createdAt", "displayName"],
11211121+ "properties": {
11221122+ "displayName": {
11231123+ "type": "string",
11241124+ "description": "A display name to be shown on a profile",
11251125+ "maxLength": 64
11261126+ },
11271127+ "description": {
11281128+ "type": "string",
11291129+ "description": "Text for user to describe themselves",
11301130+ "maxLength": 500
11311131+ },
11321132+ "avatar": {
11331133+ "type": "blob",
11341134+ "maxSize": 1000000,
11351135+ "description": "Image to show on user's profiles"
11361136+ },
11371137+ "createdAt": {
11381138+ "type": "string",
11391139+ "description": "The creation date of the profile",
11401140+ "format": "date-time"
11411141+ }
11421142+ }
11431143+ },
11441144+ "social.clippr.feed.clip": {
11451145+ "type": "object",
11461146+ "required": ["url", "title", "description", "unlisted", "createdAt"],
11471147+ "properties": {
11481148+ "url": {
11491149+ "type": "string",
11501150+ "description": "The URL of the bookmark. Cannot be left empty or be modified after creation.",
11511151+ "format": "uri",
11521152+ "maxLength": 2000
11531153+ },
11541154+ "title": {
11551155+ "type": "string",
11561156+ "description": "The title of the bookmark. If left empty, reuse the URL.",
11571157+ "maxLength": 2048
11581158+ },
11591159+ "description": {
11601160+ "type": "string",
11611161+ "description": "A description of the bookmark's content. This should be ripped from the URL metadata and be static for all records using the URL.",
11621162+ "maxLength": 4096
11631163+ },
11641164+ "notes": {
11651165+ "type": "string",
11661166+ "description": "User-written notes for the bookmark. Public and personal.",
11671167+ "maxLength": 10000
11681168+ },
11691169+ "tags": {
11701170+ "type": "array",
11711171+ "description": "An array of tags. A format of solely alphanumeric characters and dashes should be used.",
11721172+ "items": {
11731173+ "$ref": "#/components/schemas/com.atproto.repo.strongRef"
11741174+ }
11751175+ },
11761176+ "unlisted": {
11771177+ "type": "boolean",
11781178+ "description": "Whether the bookmark can be used for feed indexing and aggregation"
11791179+ },
11801180+ "unread": {
11811181+ "type": "boolean",
11821182+ "description": "Whether the bookmark has been read by the user",
11831183+ "default": true
11841184+ },
11851185+ "languages": {
11861186+ "type": "array",
11871187+ "items": {
11881188+ "type": "string",
11891189+ "format": "language"
11901190+ },
11911191+ "maxItems": 5
11921192+ },
11931193+ "createdAt": {
11941194+ "type": "string",
11951195+ "description": "Client-declared timestamp when the bookmark is created",
11961196+ "format": "date-time"
11971197+ }
11981198+ }
11991199+ },
12001200+ "social.clippr.feed.tag": {
12011201+ "type": "object",
12021202+ "required": ["name", "createdAt"],
12031203+ "properties": {
12041204+ "name": {
12051205+ "type": "string",
12061206+ "description": "A de-duplicated string containing the name of the tag",
12071207+ "maxLength": 64
12081208+ },
12091209+ "color": {
12101210+ "type": "string",
12111211+ "description": "A hexadecimal color code",
12121212+ "maxLength": 7
12131213+ },
12141214+ "description": {
12151215+ "type": "string",
12161216+ "description": "A description of the tag for additional context",
12171217+ "maxLength": 5000
12181218+ },
12191219+ "createdAt": {
12201220+ "type": "string",
12211221+ "description": "A client-defined timestamp for the creation of the tag",
12221222+ "format": "date-time"
12231223+ }
12241224+ }
12251225+ }
12261226+ }
12271227+ }
12921228}