diff --git a/lexicons/blue/microcosm/identity/resolveMiniDoc.json b/lexicons/blue/microcosm/identity/resolveMiniDoc.json new file mode 100644 index 0000000000..fad411d62d --- /dev/null +++ b/lexicons/blue/microcosm/identity/resolveMiniDoc.json @@ -0,0 +1,50 @@ +{ + "id": "blue.microcosm.identity.resolveMiniDoc", + "defs": { + "main": { + "type": "query", + "description": "Slingshot: like com.atproto.identity.resolveIdentity but instead of the full didDoc it returns an atproto-relevant subset", + "parameters": { + "type": "params", + "required": ["identifier"], + "properties": { + "identifier": { + "type": "string", + "format": "at-identifier", + "description": "handle or DID to resolve" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["did", "handle", "pds", "signing_key"], + "properties": { + "did": { + "type": "string", + "format": "did", + "description": "DID, bi-directionally verified if a handle was provided in the query" + }, + "handle": { + "type": "string", + "format": "handle", + "description": "the validated handle of the account or 'handle.invalid' if the handle did not bi-directionally match the DID document" + }, + "pds": { + "type": "string", + "format": "uri", + "description": "the identity's PDS URL" + }, + "signing_key": { + "type": "string", + "description": "the atproto signing key publicKeyMultibase" + } + } + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/blue/microcosm/links/getBacklinks.json b/lexicons/blue/microcosm/links/getBacklinks.json new file mode 100644 index 0000000000..e6e3d9d02a --- /dev/null +++ b/lexicons/blue/microcosm/links/getBacklinks.json @@ -0,0 +1,87 @@ +{ + "id": "blue.microcosm.links.getBacklinks", + "defs": { + "main": { + "type": "query", + "description": "Constellation: list records linking to any record, identity, or uri", + "parameters": { + "type": "params", + "required": ["subject", "source"], + "properties": { + "subject": { + "type": "string", + "format": "uri", + "description": "the target being linked to (at-uri, did, or uri)" + }, + "source": { + "type": "string", + "description": "collection and path specification (e.g., 'app.bsky.feed.like:subject.uri')" + }, + "did": { + "type": "array", + "description": "filter links to those from specific users", + "items": { + "type": "string", + "format": "did" + } + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 16, + "description": "number of results to return" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["total", "records"], + "properties": { + "total": { + "type": "integer", + "description": "total number of matching links" + }, + "records": { + "type": "array", + "items": { + "type": "ref", + "ref": "#linkRecord" + } + }, + "cursor": { + "type": "string", + "description": "pagination cursor" + } + } + } + } + }, + "linkRecord": { + "type": "object", + "description": "a record linking to the subject", + "required": ["did", "collection", "rkey"], + "properties": { + "did": { + "type": "string", + "format": "did", + "description": "the DID of the linking record's repository" + }, + "collection": { + "type": "string", + "format": "nsid", + "description": "the collection of the linking record" + }, + "rkey": { + "type": "string", + "format": "record-key", + "description": "the record key of the linking record" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/blue/microcosm/links/getBacklinksCount.json b/lexicons/blue/microcosm/links/getBacklinksCount.json new file mode 100644 index 0000000000..d8ee34f7bc --- /dev/null +++ b/lexicons/blue/microcosm/links/getBacklinksCount.json @@ -0,0 +1,39 @@ +{ + "id": "blue.microcosm.links.getBacklinksCount", + "defs": { + "main": { + "type": "query", + "description": "Constellation: count records that link to another record", + "parameters": { + "type": "params", + "required": ["subject", "source"], + "properties": { + "subject": { + "type": "string", + "format": "at-uri", + "description": "the target being linked to (at-uri, did, or uri)" + }, + "source": { + "type": "string", + "description": "collection and path specification for the primary link" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["total"], + "properties": { + "total": { + "type": "integer", + "description": "total number of matching links" + } + } + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/blue/microcosm/links/getManyToManyCounts.json b/lexicons/blue/microcosm/links/getManyToManyCounts.json new file mode 100644 index 0000000000..3363509ae9 --- /dev/null +++ b/lexicons/blue/microcosm/links/getManyToManyCounts.json @@ -0,0 +1,91 @@ +{ + "id": "blue.microcosm.links.getManyToManyCounts", + "defs": { + "main": { + "type": "query", + "description": "Constellation: count many-to-many relationships with secondary link paths", + "parameters": { + "type": "params", + "required": ["subject", "source", "pathToOther"], + "properties": { + "subject": { + "type": "string", + "format": "uri", + "description": "the primary target being linked to (at-uri, did, or uri)" + }, + "source": { + "type": "string", + "description": "collection and path specification for the primary link" + }, + "pathToOther": { + "type": "string", + "description": "path to the secondary link in the many-to-many record (e.g., 'otherThing.uri')" + }, + "did": { + "type": "array", + "description": "filter links to those from specific users", + "items": { + "type": "string", + "format": "did" + } + }, + "otherSubject": { + "type": "array", + "description": "filter secondary links to specific subjects", + "items": { + "type": "string" + } + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 16, + "description": "number of results to return" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["counts_by_other_subject"], + "properties": { + "counts_by_other_subject": { + "type": "array", + "items": { + "type": "ref", + "ref": "#countBySubject" + } + }, + "cursor": { + "type": "string", + "description": "pagination cursor" + } + } + } + } + }, + "countBySubject": { + "type": "object", + "description": "count of links to a secondary subject", + "required": ["subject", "total", "distinct"], + "properties": { + "subject": { + "type": "string", + "description": "the secondary subject being counted" + }, + "total": { + "type": "integer", + "description": "total number of links to this subject" + }, + "distinct": { + "type": "integer", + "description": "number of distinct DIDs linking to this subject" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/blue/microcosm/repo/get-record-by-uri.ts b/lexicons/blue/microcosm/repo/get-record-by-uri.ts new file mode 100644 index 0000000000..01b4f2fd29 --- /dev/null +++ b/lexicons/blue/microcosm/repo/get-record-by-uri.ts @@ -0,0 +1,44 @@ +import { + document, + object, + params, + query, + required, + string, + unknown, +} from '@atcute/lexicon-doc/builder' + +export default document({ + id: 'blue.microcosm.repo.getRecordByUri', + defs: { + main: query({ + description: + 'Slingshot: ergonomic complement to com.atproto.repo.getRecord which accepts an at-uri instead of individual repo/collection/rkey params', + parameters: params({ + properties: { + at_uri: required( + string({ + format: 'at-uri', + description: 'the at-uri of the record (identifier can be a DID or handle)', + }), + ), + cid: string({ + format: 'cid', + description: + 'optional CID of the version of the record. if not specified, return the most recent version. if specified and a newer version exists, returns 404.', + }), + }, + }), + output: { + encoding: 'application/json', + schema: object({ + properties: { + uri: required(string({ format: 'at-uri', description: 'at-uri for this record' })), + cid: string({ format: 'cid', description: 'CID for this exact version of the record' }), + value: required(unknown({ description: 'the record itself' })), + }, + }), + }, + }), + }, +}) diff --git a/lexicons/blue/microcosm/repo/getRecordByUri.json b/lexicons/blue/microcosm/repo/getRecordByUri.json new file mode 100644 index 0000000000..e99472e0bb --- /dev/null +++ b/lexicons/blue/microcosm/repo/getRecordByUri.json @@ -0,0 +1,50 @@ +{ + "id": "blue.microcosm.repo.getRecordByUri", + "defs": { + "main": { + "type": "query", + "description": "Slingshot: ergonomic complement to com.atproto.repo.getRecord which accepts an at-uri instead of individual repo/collection/rkey params", + "parameters": { + "type": "params", + "required": ["at_uri"], + "properties": { + "at_uri": { + "type": "string", + "format": "at-uri", + "description": "the at-uri of the record (identifier can be a DID or handle)" + }, + "cid": { + "type": "string", + "format": "cid", + "description": "optional CID of the version of the record. if not specified, return the most recent version. if specified and a newer version exists, returns 404." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["uri", "value"], + "properties": { + "uri": { + "type": "string", + "format": "at-uri", + "description": "at-uri for this record" + }, + "cid": { + "type": "string", + "format": "cid", + "description": "CID for this exact version of the record" + }, + "value": { + "type": "unknown", + "description": "the record itself" + } + } + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/server/api/social/profile/[...handle].get.ts b/server/api/social/profile/[...handle].get.ts deleted file mode 100644 index a0640de869..0000000000 --- a/server/api/social/profile/[...handle].get.ts +++ /dev/null @@ -1,14 +0,0 @@ -export default defineEventHandler(async event => { - const handle = getRouterParam(event, 'handle') - if (!handle) { - throw createError({ - status: 400, - message: 'handle not provided', - }) - } - - const profileUtil = new ProfileUtils() - const profile = await profileUtil.getProfile(handle) - console.log('ENDPOINT', { handle, profile }) - return profile -}) diff --git a/server/api/social/profile/[identifier]/index.get.ts b/server/api/social/profile/[identifier]/index.get.ts new file mode 100644 index 0000000000..73d88eae40 --- /dev/null +++ b/server/api/social/profile/[identifier]/index.get.ts @@ -0,0 +1,14 @@ +export default defineEventHandler(async event => { + const identifier = getRouterParam(event, 'identifier') + if (!identifier) { + throw createError({ + status: 400, + message: 'identifier not provided', + }) + } + + const profileUtil = new ProfileUtils() + const profile = await profileUtil.getProfile(identifier) + console.log('ENDPOINT', { identifier, profile }) + return profile +}) diff --git a/server/api/social/profile/[identifier]/likes.get.ts b/server/api/social/profile/[identifier]/likes.get.ts new file mode 100644 index 0000000000..1a2745f2fe --- /dev/null +++ b/server/api/social/profile/[identifier]/likes.get.ts @@ -0,0 +1,10 @@ +import { IdentityUtils } from '#server/utils/atproto/utils/identity' + +export default defineEventHandler(async event => { + const identifier = getRouterParam(event, 'identifier') + const utils = new IdentityUtils() + const minidoc = await utils.getMiniDoc(identifier || '') + const likesUtil = new PackageLikesUtils() + + return likesUtil.getUserLikes(minidoc) +}) diff --git a/server/utils/atproto/utils/identity.ts b/server/utils/atproto/utils/identity.ts new file mode 100644 index 0000000000..05c33df722 --- /dev/null +++ b/server/utils/atproto/utils/identity.ts @@ -0,0 +1,40 @@ +import { Client } from '@atproto/lex' +import { ensureValidAtIdentifier } from '@atproto/syntax' +import * as blue from '#shared/types/lexicons/blue' + +const HEADERS = { 'User-Agent': 'npmx' } + +// Aggersive cache on identity since that doesn't change a ton +const CACHE_MAX_AGE_IDENTITY = CACHE_MAX_AGE_ONE_HOUR * 6 + +const CACHE_KEY_IDENTITY = (identity: string) => `identity:${identity}` + +export class IdentityUtils { + private readonly cache: CacheAdapter + private readonly slingShotClient: Client + constructor() { + this.cache = getCacheAdapter('generic') + this.slingShotClient = new Client(`https://${SLINGSHOT_HOST}`, { + headers: HEADERS, + }) + } + + /** + * Gets the user's mini doc from slingshot + * @param identifier - A users did or handle + * @returns + */ + async getMiniDoc(identifier: string): Promise { + ensureValidAtIdentifier(identifier) + const cacheKey = CACHE_KEY_IDENTITY(identifier) + const cached = await this.cache.get(cacheKey) + if (cached) { + return cached + } + const result = await this.slingShotClient.call(blue.microcosm.identity.resolveMiniDoc, { + identifier: identifier, + }) + await this.cache.set(cacheKey, result, CACHE_MAX_AGE_IDENTITY) + return result + } +} diff --git a/server/utils/atproto/utils/likes.ts b/server/utils/atproto/utils/likes.ts index fe71c25926..1bf3919c5b 100644 --- a/server/utils/atproto/utils/likes.ts +++ b/server/utils/atproto/utils/likes.ts @@ -1,5 +1,8 @@ import { $nsid as likeNsid } from '#shared/types/lexicons/dev/npmx/feed/like.defs' import type { Backlink } from '#shared/utils/constellation' +import type * as blue from '#shared/types/lexicons/blue' +import * as dev from '#shared/types/lexicons/dev' +import { Client } from '@atproto/lex' //Cache keys and helpers const CACHE_PREFIX = 'atproto-likes:' @@ -248,4 +251,24 @@ export class PackageLikesUtils { userHasLiked: false, } } + + /** + * Gets a list of likes for a user. Newest first + * @param miniDoc + * @param limit + * @returns + */ + async getUserLikes( + miniDoc: blue.microcosm.identity.resolveMiniDoc.OutputBody, + limit: number = 10, + ) { + const client = new Client(miniDoc.pds, { + headers: { 'User-Agent': 'npmx' }, + }) + const result = client.list(dev.npmx.feed.like, { + limit, + repo: miniDoc.did, + }) + return result + } }