diff --git a/apps/api/lexicons/graph/defs.json b/apps/api/lexicons/graph/defs.json new file mode 100644 index 00000000..059b923a --- /dev/null +++ b/apps/api/lexicons/graph/defs.json @@ -0,0 +1,45 @@ +{ + "lexicon": 1, + "id": "app.rocksky.graph.defs", + "defs": { + "notFoundActor": { + "type": "object", + "description": "indicates that a handle or DID could not be resolved", + "required": [ + "actor", + "notFound" + ], + "properties": { + "actor": { + "type": "string", + "format": "at-identifier" + }, + "notFound": { + "type": "boolean" + } + } + }, + "relationship": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "following": { + "type": "string", + "description": "if the actor follows this DID, this is the AT-URI of the follow record", + "format": "at-uri" + }, + "followedBy": { + "type": "string", + "description": "if the actor is followed by this DID, contains the AT-URI of the follow record", + "format": "at-uri" + } + } + } + } +} diff --git a/apps/api/lexicons/graph/follow.json b/apps/api/lexicons/graph/follow.json new file mode 100644 index 00000000..d97078e3 --- /dev/null +++ b/apps/api/lexicons/graph/follow.json @@ -0,0 +1,28 @@ +{ + "lexicon": 1, + "id": "app.rocksky.graph.follow", + "defs": { + "main": { + "type": "record", + "description": "Record declaring a social 'follow' relationship of another account.", + "key": "tid", + "record": { + "type": "object", + "required": [ + "createdAt", + "subject" + ], + "properties": { + "createdAt": { + "type": "string", + "format": "datetime" + }, + "subject": { + "type": "ref", + "ref": "com.atproto.repo.strongRef" + } + } + } + } + } +} diff --git a/apps/api/lexicons/graph/getFollowers.json b/apps/api/lexicons/graph/getFollowers.json new file mode 100644 index 00000000..cad48fa9 --- /dev/null +++ b/apps/api/lexicons/graph/getFollowers.json @@ -0,0 +1,58 @@ +{ + "lexicon": 1, + "id": "app.rocksky.graph.getFollowers", + "defs": { + "main": { + "type": "query", + "description": "Enumerates accounts which follow a specified account (actor).", + "parameters": { + "type": "params", + "required": [ + "actor" + ], + "properties": { + "actor": { + "type": "string", + "format": "at-identifier" + }, + "limit": { + "type": "integer", + "maximum": 100, + "minimum": 1, + "default": 50 + }, + "cursor": { + "type": "string" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "subject", + "followers" + ], + "properties": { + "subject": { + "type": "ref", + "ref": "app.rocksky.actor.defs#profileViewBasic" + }, + "followers": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.rocksky.actor.defs#profileViewBasic" + } + }, + "cursor": { + "type": "string", + "description": "A cursor value to pass to subsequent calls to get the next page of results." + } + } + } + } + } + } +} diff --git a/apps/api/lexicons/graph/getFollows.json b/apps/api/lexicons/graph/getFollows.json new file mode 100644 index 00000000..3fd6313b --- /dev/null +++ b/apps/api/lexicons/graph/getFollows.json @@ -0,0 +1,58 @@ +{ + "lexicon": 1, + "id": "app.rocksky.graph.getFollows", + "defs": { + "main": { + "type": "query", + "description": "Enumerates accounts which a specified account (actor) follows.", + "parameters": { + "type": "params", + "required": [ + "actor" + ], + "properties": { + "actor": { + "type": "string", + "format": "at-identifier" + }, + "limit": { + "type": "integer", + "maximum": 100, + "minimum": 1, + "default": 50 + }, + "cursor": { + "type": "string" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "subject", + "follows" + ], + "properties": { + "subject": { + "type": "ref", + "ref": "app.rocksky.actor.defs#profileViewBasic" + }, + "follows": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.rocksky.actor.defs#profileViewBasic" + } + }, + "cursor": { + "type": "string", + "description": "A cursor value to pass to subsequent calls to get the next page of results." + } + } + } + } + } + } +} diff --git a/apps/api/pkl/defs/graph/defs.pkl b/apps/api/pkl/defs/graph/defs.pkl new file mode 100644 index 00000000..2bf5f6d9 --- /dev/null +++ b/apps/api/pkl/defs/graph/defs.pkl @@ -0,0 +1,41 @@ +amends "../../schema/lexicon.pkl" + +lexicon = 1 +id = "app.rocksky.graph.defs" +defs = new Mapping { + ["notFoundActor"] = new ObjectType { + type = "object" + description = "indicates that a handle or DID could not be resolved" + required = List("actor", "notFound") + properties { + ["actor"] = new StringType { + type = "string" + format = "at-identifier" + } + ["notFound"] = new BooleanType { + type = "boolean" + } + } + } + ["relationship"] { + type = "object" + required = List("did") + properties { + ["did"] = new StringType { + type = "string" + format = "did" + } + ["following"] = new StringType { + type = "string" + format = "at-uri" + description = "if the actor follows this DID, this is the AT-URI of the follow record" + } + ["followedBy"] = new StringType { + type = "string" + format = "at-uri" + description = + "if the actor is followed by this DID, contains the AT-URI of the follow record" + } + } + } +} diff --git a/apps/api/pkl/defs/graph/follow.pkl b/apps/api/pkl/defs/graph/follow.pkl new file mode 100644 index 00000000..5c83827a --- /dev/null +++ b/apps/api/pkl/defs/graph/follow.pkl @@ -0,0 +1,26 @@ +amends "../../schema/lexicon.pkl" + +lexicon = 1 +id = "app.rocksky.graph.follow" +defs = new Mapping { + ["main"] { + type = "record" + key = "tid" + description = "Record declaring a social 'follow' relationship of another account." + `record` { + type = "object" + required = List("createdAt", "subject") + properties { + ["createdAt"] = new StringType { + type = "string" + format = "datetime" + } + + ["subject"] = new Ref { + type = "ref" + ref = "com.atproto.repo.strongRef" + } + } + } + } +} diff --git a/apps/api/pkl/defs/graph/getFollowers.pkl b/apps/api/pkl/defs/graph/getFollowers.pkl new file mode 100644 index 00000000..75a5c1a9 --- /dev/null +++ b/apps/api/pkl/defs/graph/getFollowers.pkl @@ -0,0 +1,54 @@ +amends "../../schema/lexicon.pkl" + +lexicon = 1 +id = "app.rocksky.graph.getFollowers" +defs = new Mapping { + ["main"] { + type = "query" + description = "Enumerates accounts which follow a specified account (actor)." + parameters = new Params { + required = List("actor") + properties { + ["actor"] = new StringType { + type = "string" + format = "at-identifier" + } + ["limit"] = new IntegerType { + type = "integer" + maximum = 100 + minimum = 1 + default = 50 + } + ["cursor"] = new StringType { + type = "string" + } + } + } + + output { + encoding = "application/json" + schema = new ObjectType { + type = "object" + required = List("subject", "followers") + properties { + ["subject"] = new Ref { + type = "ref" + ref = "app.rocksky.actor.defs#profileViewBasic" + } + ["followers"] = new Array { + type = "array" + items = new Ref { + type = "ref" + ref = "app.rocksky.actor.defs#profileViewBasic" + } + } + ["cursor"] = new StringType { + type = "string" + description = + "A cursor value to pass to subsequent calls to get the next page of results." + } + } + } + } + } +} diff --git a/apps/api/pkl/defs/graph/getFollows.pkl b/apps/api/pkl/defs/graph/getFollows.pkl new file mode 100644 index 00000000..bfcef26c --- /dev/null +++ b/apps/api/pkl/defs/graph/getFollows.pkl @@ -0,0 +1,54 @@ +amends "../../schema/lexicon.pkl" + +lexicon = 1 +id = "app.rocksky.graph.getFollows" +defs = new Mapping { + ["main"] { + type = "query" + description = "Enumerates accounts which a specified account (actor) follows." + parameters = new Params { + required = List("actor") + properties { + ["actor"] = new StringType { + type = "string" + format = "at-identifier" + } + ["limit"] = new IntegerType { + type = "integer" + maximum = 100 + minimum = 1 + default = 50 + } + ["cursor"] = new StringType { + type = "string" + } + } + } + + output { + encoding = "application/json" + schema = new ObjectType { + type = "object" + required = List("subject", "follows") + properties { + ["subject"] = new Ref { + type = "ref" + ref = "app.rocksky.actor.defs#profileViewBasic" + } + ["follows"] = new Array { + type = "array" + items = new Ref { + type = "ref" + ref = "app.rocksky.actor.defs#profileViewBasic" + } + } + ["cursor"] = new StringType { + type = "string" + description = + "A cursor value to pass to subsequent calls to get the next page of results." + } + } + } + } + } +} diff --git a/apps/api/pkl/schema/lexicon.pkl b/apps/api/pkl/schema/lexicon.pkl index c8af9c51..17006236 100644 --- a/apps/api/pkl/schema/lexicon.pkl +++ b/apps/api/pkl/schema/lexicon.pkl @@ -13,7 +13,9 @@ class StringType extends BaseType { class IntegerType extends BaseType { type: "integer" + maximum: Int? minimum: Int? + default: Int? } class BooleanType extends BaseType { @@ -40,7 +42,7 @@ class Params { class ObjectType extends BaseType { type: "object" required: List? - properties: Mapping + properties: Mapping } class Union extends BaseType { diff --git a/apps/api/src/lexicon/index.ts b/apps/api/src/lexicon/index.ts index b54be268..64c4b23c 100644 --- a/apps/api/src/lexicon/index.ts +++ b/apps/api/src/lexicon/index.ts @@ -48,6 +48,8 @@ import type * as AppRockskyFeedSearch from "./types/app/rocksky/feed/search"; import type * as AppRockskyGoogledriveDownloadFile from "./types/app/rocksky/googledrive/downloadFile"; import type * as AppRockskyGoogledriveGetFile from "./types/app/rocksky/googledrive/getFile"; import type * as AppRockskyGoogledriveGetFiles from "./types/app/rocksky/googledrive/getFiles"; +import type * as AppRockskyGraphGetFollowers from "./types/app/rocksky/graph/getFollowers"; +import type * as AppRockskyGraphGetFollows from "./types/app/rocksky/graph/getFollows"; import type * as AppRockskyLikeDislikeShout from "./types/app/rocksky/like/dislikeShout"; import type * as AppRockskyLikeDislikeSong from "./types/app/rocksky/like/dislikeSong"; import type * as AppRockskyLikeLikeShout from "./types/app/rocksky/like/likeShout"; @@ -237,6 +239,7 @@ export class AppRockskyNS { dropbox: AppRockskyDropboxNS; feed: AppRockskyFeedNS; googledrive: AppRockskyGoogledriveNS; + graph: AppRockskyGraphNS; like: AppRockskyLikeNS; player: AppRockskyPlayerNS; playlist: AppRockskyPlaylistNS; @@ -256,6 +259,7 @@ export class AppRockskyNS { this.dropbox = new AppRockskyDropboxNS(server); this.feed = new AppRockskyFeedNS(server); this.googledrive = new AppRockskyGoogledriveNS(server); + this.graph = new AppRockskyGraphNS(server); this.like = new AppRockskyLikeNS(server); this.player = new AppRockskyPlayerNS(server); this.playlist = new AppRockskyPlaylistNS(server); @@ -705,6 +709,36 @@ export class AppRockskyGoogledriveNS { } } +export class AppRockskyGraphNS { + _server: Server; + + constructor(server: Server) { + this._server = server; + } + + getFollowers( + cfg: ConfigOf< + AV, + AppRockskyGraphGetFollowers.Handler>, + AppRockskyGraphGetFollowers.HandlerReqCtx> + >, + ) { + const nsid = "app.rocksky.graph.getFollowers"; // @ts-ignore + return this._server.xrpc.method(nsid, cfg); + } + + getFollows( + cfg: ConfigOf< + AV, + AppRockskyGraphGetFollows.Handler>, + AppRockskyGraphGetFollows.HandlerReqCtx> + >, + ) { + const nsid = "app.rocksky.graph.getFollows"; // @ts-ignore + return this._server.xrpc.method(nsid, cfg); + } +} + export class AppRockskyLikeNS { _server: Server; diff --git a/apps/api/src/lexicon/lexicons.ts b/apps/api/src/lexicon/lexicons.ts index 164169a4..607e603e 100644 --- a/apps/api/src/lexicon/lexicons.ts +++ b/apps/api/src/lexicon/lexicons.ts @@ -2735,6 +2735,184 @@ export const schemaDict = { }, }, }, + AppRockskyGraphDefs: { + lexicon: 1, + id: "app.rocksky.graph.defs", + defs: { + notFoundActor: { + type: "object", + description: "indicates that a handle or DID could not be resolved", + required: ["actor", "notFound"], + properties: { + actor: { + type: "string", + format: "at-identifier", + }, + notFound: { + type: "boolean", + }, + }, + }, + relationship: { + type: "object", + required: ["did"], + properties: { + did: { + type: "string", + format: "did", + }, + following: { + type: "string", + description: + "if the actor follows this DID, this is the AT-URI of the follow record", + format: "at-uri", + }, + followedBy: { + type: "string", + description: + "if the actor is followed by this DID, contains the AT-URI of the follow record", + format: "at-uri", + }, + }, + }, + }, + }, + AppRockskyGraphFollow: { + lexicon: 1, + id: "app.rocksky.graph.follow", + defs: { + main: { + type: "record", + description: + "Record declaring a social 'follow' relationship of another account.", + key: "tid", + record: { + type: "object", + required: ["createdAt", "subject"], + properties: { + createdAt: { + type: "string", + format: "datetime", + }, + subject: { + type: "ref", + ref: "lex:com.atproto.repo.strongRef", + }, + }, + }, + }, + }, + }, + AppRockskyGraphGetFollowers: { + lexicon: 1, + id: "app.rocksky.graph.getFollowers", + defs: { + main: { + type: "query", + description: + "Enumerates accounts which follow a specified account (actor).", + parameters: { + type: "params", + required: ["actor"], + properties: { + actor: { + type: "string", + format: "at-identifier", + }, + limit: { + type: "integer", + maximum: 100, + minimum: 1, + default: 50, + }, + cursor: { + type: "string", + }, + }, + }, + output: { + encoding: "application/json", + schema: { + type: "object", + required: ["subject", "followers"], + properties: { + subject: { + type: "ref", + ref: "lex:app.rocksky.actor.defs#profileViewBasic", + }, + followers: { + type: "array", + items: { + type: "ref", + ref: "lex:app.rocksky.actor.defs#profileViewBasic", + }, + }, + cursor: { + type: "string", + description: + "A cursor value to pass to subsequent calls to get the next page of results.", + }, + }, + }, + }, + }, + }, + }, + AppRockskyGraphGetFollows: { + lexicon: 1, + id: "app.rocksky.graph.getFollows", + defs: { + main: { + type: "query", + description: + "Enumerates accounts which a specified account (actor) follows.", + parameters: { + type: "params", + required: ["actor"], + properties: { + actor: { + type: "string", + format: "at-identifier", + }, + limit: { + type: "integer", + maximum: 100, + minimum: 1, + default: 50, + }, + cursor: { + type: "string", + }, + }, + }, + output: { + encoding: "application/json", + schema: { + type: "object", + required: ["subject", "follows"], + properties: { + subject: { + type: "ref", + ref: "lex:app.rocksky.actor.defs#profileViewBasic", + }, + follows: { + type: "array", + items: { + type: "ref", + ref: "lex:app.rocksky.actor.defs#profileViewBasic", + }, + }, + cursor: { + type: "string", + description: + "A cursor value to pass to subsequent calls to get the next page of results.", + }, + }, + }, + }, + }, + }, + }, AppRockskyLikeDislikeShout: { lexicon: 1, id: "app.rocksky.like.dislikeShout", @@ -5395,6 +5573,10 @@ export const ids = { AppRockskyGoogledriveDownloadFile: "app.rocksky.googledrive.downloadFile", AppRockskyGoogledriveGetFile: "app.rocksky.googledrive.getFile", AppRockskyGoogledriveGetFiles: "app.rocksky.googledrive.getFiles", + AppRockskyGraphDefs: "app.rocksky.graph.defs", + AppRockskyGraphFollow: "app.rocksky.graph.follow", + AppRockskyGraphGetFollowers: "app.rocksky.graph.getFollowers", + AppRockskyGraphGetFollows: "app.rocksky.graph.getFollows", AppRockskyLikeDislikeShout: "app.rocksky.like.dislikeShout", AppRockskyLikeDislikeSong: "app.rocksky.like.dislikeSong", AppRockskyLike: "app.rocksky.like", diff --git a/apps/api/src/lexicon/types/app/rocksky/graph/defs.ts b/apps/api/src/lexicon/types/app/rocksky/graph/defs.ts new file mode 100644 index 00000000..654ae397 --- /dev/null +++ b/apps/api/src/lexicon/types/app/rocksky/graph/defs.ts @@ -0,0 +1,47 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { type ValidationResult, BlobRef } from "@atproto/lexicon"; +import { lexicons } from "../../../../lexicons"; +import { isObj, hasProp } from "../../../../util"; +import { CID } from "multiformats/cid"; + +/** indicates that a handle or DID could not be resolved */ +export interface NotFoundActor { + actor: string; + notFound: boolean; + [k: string]: unknown; +} + +export function isNotFoundActor(v: unknown): v is NotFoundActor { + return ( + isObj(v) && + hasProp(v, "$type") && + v.$type === "app.rocksky.graph.defs#notFoundActor" + ); +} + +export function validateNotFoundActor(v: unknown): ValidationResult { + return lexicons.validate("app.rocksky.graph.defs#notFoundActor", v); +} + +export interface Relationship { + did: string; + /** if the actor follows this DID, this is the AT-URI of the follow record */ + following?: string; + /** if the actor is followed by this DID, contains the AT-URI of the follow record */ + followedBy?: string; + [k: string]: unknown; +} + +export function isRelationship(v: unknown): v is Relationship { + return ( + isObj(v) && + hasProp(v, "$type") && + v.$type === "app.rocksky.graph.defs#relationship" + ); +} + +export function validateRelationship(v: unknown): ValidationResult { + return lexicons.validate("app.rocksky.graph.defs#relationship", v); +} diff --git a/apps/api/src/lexicon/types/app/rocksky/graph/follow.ts b/apps/api/src/lexicon/types/app/rocksky/graph/follow.ts new file mode 100644 index 00000000..eec9ecc0 --- /dev/null +++ b/apps/api/src/lexicon/types/app/rocksky/graph/follow.ts @@ -0,0 +1,27 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import { type ValidationResult, BlobRef } from "@atproto/lexicon"; +import { lexicons } from "../../../../lexicons"; +import { isObj, hasProp } from "../../../../util"; +import { CID } from "multiformats/cid"; +import type * as ComAtprotoRepoStrongRef from "../../../com/atproto/repo/strongRef"; + +export interface Record { + createdAt: string; + subject: ComAtprotoRepoStrongRef.Main; + [k: string]: unknown; +} + +export function isRecord(v: unknown): v is Record { + return ( + isObj(v) && + hasProp(v, "$type") && + (v.$type === "app.rocksky.graph.follow#main" || + v.$type === "app.rocksky.graph.follow") + ); +} + +export function validateRecord(v: unknown): ValidationResult { + return lexicons.validate("app.rocksky.graph.follow#main", v); +} diff --git a/apps/api/src/lexicon/types/app/rocksky/graph/getFollowers.ts b/apps/api/src/lexicon/types/app/rocksky/graph/getFollowers.ts new file mode 100644 index 00000000..181407b8 --- /dev/null +++ b/apps/api/src/lexicon/types/app/rocksky/graph/getFollowers.ts @@ -0,0 +1,52 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import type express from "express"; +import { ValidationResult, BlobRef } from "@atproto/lexicon"; +import { lexicons } from "../../../../lexicons"; +import { isObj, hasProp } from "../../../../util"; +import { CID } from "multiformats/cid"; +import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; +import type * as AppRockskyActorDefs from "../actor/defs"; + +export interface QueryParams { + actor: string; + limit: number; + cursor?: string; +} + +export type InputSchema = undefined; + +export interface OutputSchema { + subject: AppRockskyActorDefs.ProfileViewBasic; + followers: AppRockskyActorDefs.ProfileViewBasic[]; + /** A cursor value to pass to subsequent calls to get the next page of results. */ + cursor?: string; + [k: string]: unknown; +} + +export type HandlerInput = undefined; + +export interface HandlerSuccess { + encoding: "application/json"; + body: OutputSchema; + headers?: { [key: string]: string }; +} + +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; +export type HandlerReqCtx = { + auth: HA; + params: QueryParams; + input: HandlerInput; + req: express.Request; + res: express.Response; + resetRouteRateLimits: () => Promise; +}; +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput; diff --git a/apps/api/src/lexicon/types/app/rocksky/graph/getFollows.ts b/apps/api/src/lexicon/types/app/rocksky/graph/getFollows.ts new file mode 100644 index 00000000..e7c75bb0 --- /dev/null +++ b/apps/api/src/lexicon/types/app/rocksky/graph/getFollows.ts @@ -0,0 +1,52 @@ +/** + * GENERATED CODE - DO NOT MODIFY + */ +import type express from "express"; +import { ValidationResult, BlobRef } from "@atproto/lexicon"; +import { lexicons } from "../../../../lexicons"; +import { isObj, hasProp } from "../../../../util"; +import { CID } from "multiformats/cid"; +import type { HandlerAuth, HandlerPipeThrough } from "@atproto/xrpc-server"; +import type * as AppRockskyActorDefs from "../actor/defs"; + +export interface QueryParams { + actor: string; + limit: number; + cursor?: string; +} + +export type InputSchema = undefined; + +export interface OutputSchema { + subject: AppRockskyActorDefs.ProfileViewBasic; + follows: AppRockskyActorDefs.ProfileViewBasic[]; + /** A cursor value to pass to subsequent calls to get the next page of results. */ + cursor?: string; + [k: string]: unknown; +} + +export type HandlerInput = undefined; + +export interface HandlerSuccess { + encoding: "application/json"; + body: OutputSchema; + headers?: { [key: string]: string }; +} + +export interface HandlerError { + status: number; + message?: string; +} + +export type HandlerOutput = HandlerError | HandlerSuccess | HandlerPipeThrough; +export type HandlerReqCtx = { + auth: HA; + params: QueryParams; + input: HandlerInput; + req: express.Request; + res: express.Response; + resetRouteRateLimits: () => Promise; +}; +export type Handler = ( + ctx: HandlerReqCtx, +) => Promise | HandlerOutput; diff --git a/apps/api/src/xrpc/app/rocksky/feed/getFeed.ts b/apps/api/src/xrpc/app/rocksky/feed/getFeed.ts index 506f1efa..88bfe071 100644 --- a/apps/api/src/xrpc/app/rocksky/feed/getFeed.ts +++ b/apps/api/src/xrpc/app/rocksky/feed/getFeed.ts @@ -10,7 +10,7 @@ import type { SelectScrobble } from "schema/scrobbles"; import type { SelectTrack } from "schema/tracks"; import type { SelectUser } from "schema/users"; import axios from "axios"; -import { HandlerAuth } from "@atproto/xrpc-server"; +import type { HandlerAuth } from "@atproto/xrpc-server"; import { env } from "lib/env"; export default function (server: Server, ctx: Context) {