Refactor feeds & post threads (#423)
* factor out feed items & mystate * propose feed items rewordk * pr feedback * revamping getTimeline * feed service & cleanup * bugfixin * update timeline snapshots * update getAuthFeed snapshot * bugfixin * revamp getPostThread * fix up sync & votes tests * bug in post thread * fix replyCount * fix ordering * move uriImgBuilder to createServices
This commit is contained in:
parent
5be13ac3e3
commit
30ab0d341b
lexicons/app/bsky/feed
packages
api/src/client
pds
39
lexicons/app/bsky/feed/feedViewPost.json
Normal file
39
lexicons/app/bsky/feed/feedViewPost.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"lexicon": 1,
|
||||
"id": "app.bsky.feed.feedViewPost",
|
||||
"defs": {
|
||||
"main": {
|
||||
"type": "object",
|
||||
"required": ["post"],
|
||||
"properties": {
|
||||
"post": {"type": "ref", "ref": "app.bsky.feed.post#view"},
|
||||
"reply": {"type": "ref", "ref": "#replyRef"},
|
||||
"reason": {"type": "union", "refs": ["#reasonTrend", "#reasonRepost"]}
|
||||
}
|
||||
},
|
||||
"replyRef": {
|
||||
"type": "object",
|
||||
"required": ["root", "parent"],
|
||||
"properties": {
|
||||
"root": {"type": "ref", "ref": "app.bsky.feed.post#view"},
|
||||
"parent": {"type": "ref", "ref": "app.bsky.feed.post#view"}
|
||||
}
|
||||
},
|
||||
"reasonTrend": {
|
||||
"type": "object",
|
||||
"required": ["by", "indexedAt"],
|
||||
"properties": {
|
||||
"by": {"type": "ref", "ref": "app.bsky.actor.ref#withInfo"},
|
||||
"indexedAt": {"type": "datetime"}
|
||||
}
|
||||
},
|
||||
"reasonRepost": {
|
||||
"type": "object",
|
||||
"required": ["by", "indexedAt"],
|
||||
"properties": {
|
||||
"by": {"type": "ref", "ref": "app.bsky.actor.ref#withInfo"},
|
||||
"indexedAt": {"type": "datetime"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -23,44 +23,11 @@
|
||||
"cursor": {"type": "string"},
|
||||
"feed": {
|
||||
"type": "array",
|
||||
"items": {"type": "ref", "ref": "#feedItem"}
|
||||
"items": {"type": "ref", "ref": "app.bsky.feed.feedViewPost"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"feedItem": {
|
||||
"type": "object",
|
||||
"required": ["uri", "cid", "author", "record", "replyCount", "repostCount", "upvoteCount", "downvoteCount", "indexedAt"],
|
||||
"properties": {
|
||||
"uri": {"type": "string"},
|
||||
"cid": {"type": "string"},
|
||||
"author": {"type": "ref", "ref": "app.bsky.actor.ref#withInfo"},
|
||||
"trendedBy": {"type": "ref", "ref": "app.bsky.actor.ref#withInfo"},
|
||||
"repostedBy": {"type": "ref", "ref": "app.bsky.actor.ref#withInfo"},
|
||||
"record": {"type": "unknown"},
|
||||
"embed": {
|
||||
"type": "union",
|
||||
"refs": [
|
||||
"app.bsky.embed.images#presented",
|
||||
"app.bsky.embed.external#presented"
|
||||
]
|
||||
},
|
||||
"replyCount": {"type": "integer"},
|
||||
"repostCount": {"type": "integer"},
|
||||
"upvoteCount": {"type": "integer"},
|
||||
"downvoteCount": {"type": "integer"},
|
||||
"indexedAt": {"type": "datetime"},
|
||||
"myState": {"type": "ref", "ref": "#myState"}
|
||||
}
|
||||
},
|
||||
"myState": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repost": {"type": "string"},
|
||||
"upvote": {"type": "string"},
|
||||
"downvote": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@
|
||||
"properties": {
|
||||
"thread": {
|
||||
"type": "union",
|
||||
"refs": ["#post", "#notFoundPost"]
|
||||
"refs": ["#threadViewPost", "#notFoundPost"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -29,32 +29,16 @@
|
||||
{"name": "NotFound"}
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"threadViewPost": {
|
||||
"type": "object",
|
||||
"required": ["uri", "cid", "author", "record", "replyCount", "repostCount", "upvoteCount", "downvoteCount", "indexedAt"],
|
||||
"required": ["post"],
|
||||
"properties": {
|
||||
"uri": {"type": "string"},
|
||||
"cid": {"type": "string"},
|
||||
"author": {"type": "ref", "ref": "app.bsky.actor.ref#withInfo"},
|
||||
"record": {"type": "unknown"},
|
||||
"embed": {
|
||||
"type": "union",
|
||||
"refs": [
|
||||
"app.bsky.embed.images#presented",
|
||||
"app.bsky.embed.external#presented"
|
||||
]
|
||||
},
|
||||
"parent": {"type": "union", "refs": ["#post", "#notFoundPost"]},
|
||||
"replyCount": {"type": "integer"},
|
||||
"post": {"type": "ref", "ref": "app.bsky.feed.post#view"},
|
||||
"parent": {"type": "union", "refs": ["#threadViewPost", "#notFoundPost"]},
|
||||
"replies": {
|
||||
"type": "array",
|
||||
"items": {"type": "union", "refs": ["#post", "#notFoundPost"]}
|
||||
},
|
||||
"repostCount": {"type": "integer"},
|
||||
"upvoteCount": {"type": "integer"},
|
||||
"downvoteCount": {"type": "integer"},
|
||||
"indexedAt": {"type": "datetime"},
|
||||
"myState": {"type": "ref", "ref": "#myState"}
|
||||
"items": {"type": "union", "refs": ["#threadViewPost", "#notFoundPost"]}
|
||||
}
|
||||
}
|
||||
},
|
||||
"notFoundPost": {
|
||||
@ -64,14 +48,6 @@
|
||||
"uri": {"type": "string"},
|
||||
"notFound": {"type": "boolean", "const": true}
|
||||
}
|
||||
},
|
||||
"myState": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repost": {"type": "string"},
|
||||
"upvote": {"type": "string"},
|
||||
"downvote": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,44 +22,11 @@
|
||||
"cursor": {"type": "string"},
|
||||
"feed": {
|
||||
"type": "array",
|
||||
"items": {"type": "ref", "ref": "#feedItem"}
|
||||
"items": {"type": "ref", "ref": "app.bsky.feed.feedViewPost"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"feedItem": {
|
||||
"type": "object",
|
||||
"required": ["uri", "cid", "author", "record", "replyCount", "repostCount", "upvoteCount", "downvoteCount", "indexedAt"],
|
||||
"properties": {
|
||||
"uri": {"type": "string"},
|
||||
"cid": {"type": "string"},
|
||||
"author": {"type": "ref", "ref": "app.bsky.actor.ref#withInfo"},
|
||||
"trendedBy": {"type": "ref", "ref": "app.bsky.actor.ref#withInfo"},
|
||||
"repostedBy": {"type": "ref", "ref": "app.bsky.actor.ref#withInfo"},
|
||||
"record": {"type": "unknown"},
|
||||
"embed": {
|
||||
"type": "union",
|
||||
"refs": [
|
||||
"app.bsky.embed.images#presented",
|
||||
"app.bsky.embed.external#presented"
|
||||
]
|
||||
},
|
||||
"replyCount": {"type": "integer"},
|
||||
"repostCount": {"type": "integer"},
|
||||
"upvoteCount": {"type": "integer"},
|
||||
"downvoteCount": {"type": "integer"},
|
||||
"indexedAt": {"type": "datetime"},
|
||||
"myState": {"type": "ref", "ref": "#myState"}
|
||||
}
|
||||
},
|
||||
"myState": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repost": {"type": "string"},
|
||||
"upvote": {"type": "string"},
|
||||
"downvote": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -48,6 +48,37 @@
|
||||
"start": {"type": "integer", "minimum": 0},
|
||||
"end": {"type": "integer", "minimum": 0}
|
||||
}
|
||||
},
|
||||
"view": {
|
||||
"type": "object",
|
||||
"required": ["uri", "cid", "author", "record", "replyCount", "repostCount", "upvoteCount", "downvoteCount", "indexedAt", "viewer"],
|
||||
"properties": {
|
||||
"uri": {"type": "string"},
|
||||
"cid": {"type": "string"},
|
||||
"author": {"type": "ref", "ref": "app.bsky.actor.ref#withInfo"},
|
||||
"record": {"type": "unknown"},
|
||||
"embed": {
|
||||
"type": "union",
|
||||
"refs": [
|
||||
"app.bsky.embed.images#presented",
|
||||
"app.bsky.embed.external#presented"
|
||||
]
|
||||
},
|
||||
"replyCount": {"type": "integer"},
|
||||
"repostCount": {"type": "integer"},
|
||||
"upvoteCount": {"type": "integer"},
|
||||
"downvoteCount": {"type": "integer"},
|
||||
"indexedAt": {"type": "datetime"},
|
||||
"viewer": {"type": "ref", "ref": "#viewerState"}
|
||||
}
|
||||
},
|
||||
"viewerState": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"repost": {"type": "string"},
|
||||
"upvote": {"type": "string"},
|
||||
"downvote": {"type": "string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ import * as AppBskyActorSearchTypeahead from './types/app/bsky/actor/searchTypea
|
||||
import * as AppBskyActorUpdateProfile from './types/app/bsky/actor/updateProfile'
|
||||
import * as AppBskyEmbedExternal from './types/app/bsky/embed/external'
|
||||
import * as AppBskyEmbedImages from './types/app/bsky/embed/images'
|
||||
import * as AppBskyFeedFeedViewPost from './types/app/bsky/feed/feedViewPost'
|
||||
import * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed'
|
||||
import * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread'
|
||||
import * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy'
|
||||
@ -102,6 +103,7 @@ export * as AppBskyActorSearchTypeahead from './types/app/bsky/actor/searchTypea
|
||||
export * as AppBskyActorUpdateProfile from './types/app/bsky/actor/updateProfile'
|
||||
export * as AppBskyEmbedExternal from './types/app/bsky/embed/external'
|
||||
export * as AppBskyEmbedImages from './types/app/bsky/embed/images'
|
||||
export * as AppBskyFeedFeedViewPost from './types/app/bsky/feed/feedViewPost'
|
||||
export * as AppBskyFeedGetAuthorFeed from './types/app/bsky/feed/getAuthorFeed'
|
||||
export * as AppBskyFeedGetPostThread from './types/app/bsky/feed/getPostThread'
|
||||
export * as AppBskyFeedGetRepostedBy from './types/app/bsky/feed/getRepostedBy'
|
||||
|
@ -1566,6 +1566,73 @@ export const schemaDict = {
|
||||
},
|
||||
},
|
||||
},
|
||||
AppBskyFeedFeedViewPost: {
|
||||
lexicon: 1,
|
||||
id: 'app.bsky.feed.feedViewPost',
|
||||
defs: {
|
||||
main: {
|
||||
type: 'object',
|
||||
required: ['post'],
|
||||
properties: {
|
||||
post: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.feed.post#view',
|
||||
},
|
||||
reply: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.feed.feedViewPost#replyRef',
|
||||
},
|
||||
reason: {
|
||||
type: 'union',
|
||||
refs: [
|
||||
'lex:app.bsky.feed.feedViewPost#reasonTrend',
|
||||
'lex:app.bsky.feed.feedViewPost#reasonRepost',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
replyRef: {
|
||||
type: 'object',
|
||||
required: ['root', 'parent'],
|
||||
properties: {
|
||||
root: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.feed.post#view',
|
||||
},
|
||||
parent: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.feed.post#view',
|
||||
},
|
||||
},
|
||||
},
|
||||
reasonTrend: {
|
||||
type: 'object',
|
||||
required: ['by', 'indexedAt'],
|
||||
properties: {
|
||||
by: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.actor.ref#withInfo',
|
||||
},
|
||||
indexedAt: {
|
||||
type: 'datetime',
|
||||
},
|
||||
},
|
||||
},
|
||||
reasonRepost: {
|
||||
type: 'object',
|
||||
required: ['by', 'indexedAt'],
|
||||
properties: {
|
||||
by: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.actor.ref#withInfo',
|
||||
},
|
||||
indexedAt: {
|
||||
type: 'datetime',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AppBskyFeedGetAuthorFeed: {
|
||||
lexicon: 1,
|
||||
id: 'app.bsky.feed.getAuthorFeed',
|
||||
@ -1604,90 +1671,13 @@ export const schemaDict = {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.feed.getAuthorFeed#feedItem',
|
||||
ref: 'lex:app.bsky.feed.feedViewPost',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
feedItem: {
|
||||
type: 'object',
|
||||
required: [
|
||||
'uri',
|
||||
'cid',
|
||||
'author',
|
||||
'record',
|
||||
'replyCount',
|
||||
'repostCount',
|
||||
'upvoteCount',
|
||||
'downvoteCount',
|
||||
'indexedAt',
|
||||
],
|
||||
properties: {
|
||||
uri: {
|
||||
type: 'string',
|
||||
},
|
||||
cid: {
|
||||
type: 'string',
|
||||
},
|
||||
author: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.actor.ref#withInfo',
|
||||
},
|
||||
trendedBy: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.actor.ref#withInfo',
|
||||
},
|
||||
repostedBy: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.actor.ref#withInfo',
|
||||
},
|
||||
record: {
|
||||
type: 'unknown',
|
||||
},
|
||||
embed: {
|
||||
type: 'union',
|
||||
refs: [
|
||||
'lex:app.bsky.embed.images#presented',
|
||||
'lex:app.bsky.embed.external#presented',
|
||||
],
|
||||
},
|
||||
replyCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
repostCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
upvoteCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
downvoteCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
indexedAt: {
|
||||
type: 'datetime',
|
||||
},
|
||||
myState: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.feed.getAuthorFeed#myState',
|
||||
},
|
||||
},
|
||||
},
|
||||
myState: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
repost: {
|
||||
type: 'string',
|
||||
},
|
||||
upvote: {
|
||||
type: 'string',
|
||||
},
|
||||
downvote: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AppBskyFeedGetPostThread: {
|
||||
@ -1717,7 +1707,7 @@ export const schemaDict = {
|
||||
thread: {
|
||||
type: 'union',
|
||||
refs: [
|
||||
'lex:app.bsky.feed.getPostThread#post',
|
||||
'lex:app.bsky.feed.getPostThread#threadViewPost',
|
||||
'lex:app.bsky.feed.getPostThread#notFoundPost',
|
||||
],
|
||||
},
|
||||
@ -1730,76 +1720,31 @@ export const schemaDict = {
|
||||
},
|
||||
],
|
||||
},
|
||||
post: {
|
||||
threadViewPost: {
|
||||
type: 'object',
|
||||
required: [
|
||||
'uri',
|
||||
'cid',
|
||||
'author',
|
||||
'record',
|
||||
'replyCount',
|
||||
'repostCount',
|
||||
'upvoteCount',
|
||||
'downvoteCount',
|
||||
'indexedAt',
|
||||
],
|
||||
required: ['post'],
|
||||
properties: {
|
||||
uri: {
|
||||
type: 'string',
|
||||
},
|
||||
cid: {
|
||||
type: 'string',
|
||||
},
|
||||
author: {
|
||||
post: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.actor.ref#withInfo',
|
||||
},
|
||||
record: {
|
||||
type: 'unknown',
|
||||
},
|
||||
embed: {
|
||||
type: 'union',
|
||||
refs: [
|
||||
'lex:app.bsky.embed.images#presented',
|
||||
'lex:app.bsky.embed.external#presented',
|
||||
],
|
||||
ref: 'lex:app.bsky.feed.post#view',
|
||||
},
|
||||
parent: {
|
||||
type: 'union',
|
||||
refs: [
|
||||
'lex:app.bsky.feed.getPostThread#post',
|
||||
'lex:app.bsky.feed.getPostThread#threadViewPost',
|
||||
'lex:app.bsky.feed.getPostThread#notFoundPost',
|
||||
],
|
||||
},
|
||||
replyCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
replies: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'union',
|
||||
refs: [
|
||||
'lex:app.bsky.feed.getPostThread#post',
|
||||
'lex:app.bsky.feed.getPostThread#threadViewPost',
|
||||
'lex:app.bsky.feed.getPostThread#notFoundPost',
|
||||
],
|
||||
},
|
||||
},
|
||||
repostCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
upvoteCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
downvoteCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
indexedAt: {
|
||||
type: 'datetime',
|
||||
},
|
||||
myState: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.feed.getPostThread#myState',
|
||||
},
|
||||
},
|
||||
},
|
||||
notFoundPost: {
|
||||
@ -1815,20 +1760,6 @@ export const schemaDict = {
|
||||
},
|
||||
},
|
||||
},
|
||||
myState: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
repost: {
|
||||
type: 'string',
|
||||
},
|
||||
upvote: {
|
||||
type: 'string',
|
||||
},
|
||||
downvote: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AppBskyFeedGetRepostedBy: {
|
||||
@ -1952,90 +1883,13 @@ export const schemaDict = {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.feed.getTimeline#feedItem',
|
||||
ref: 'lex:app.bsky.feed.feedViewPost',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
feedItem: {
|
||||
type: 'object',
|
||||
required: [
|
||||
'uri',
|
||||
'cid',
|
||||
'author',
|
||||
'record',
|
||||
'replyCount',
|
||||
'repostCount',
|
||||
'upvoteCount',
|
||||
'downvoteCount',
|
||||
'indexedAt',
|
||||
],
|
||||
properties: {
|
||||
uri: {
|
||||
type: 'string',
|
||||
},
|
||||
cid: {
|
||||
type: 'string',
|
||||
},
|
||||
author: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.actor.ref#withInfo',
|
||||
},
|
||||
trendedBy: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.actor.ref#withInfo',
|
||||
},
|
||||
repostedBy: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.actor.ref#withInfo',
|
||||
},
|
||||
record: {
|
||||
type: 'unknown',
|
||||
},
|
||||
embed: {
|
||||
type: 'union',
|
||||
refs: [
|
||||
'lex:app.bsky.embed.images#presented',
|
||||
'lex:app.bsky.embed.external#presented',
|
||||
],
|
||||
},
|
||||
replyCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
repostCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
upvoteCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
downvoteCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
indexedAt: {
|
||||
type: 'datetime',
|
||||
},
|
||||
myState: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.feed.getTimeline#myState',
|
||||
},
|
||||
},
|
||||
},
|
||||
myState: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
repost: {
|
||||
type: 'string',
|
||||
},
|
||||
upvote: {
|
||||
type: 'string',
|
||||
},
|
||||
downvote: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AppBskyFeedGetVotes: {
|
||||
@ -2202,6 +2056,76 @@ export const schemaDict = {
|
||||
},
|
||||
},
|
||||
},
|
||||
view: {
|
||||
type: 'object',
|
||||
required: [
|
||||
'uri',
|
||||
'cid',
|
||||
'author',
|
||||
'record',
|
||||
'replyCount',
|
||||
'repostCount',
|
||||
'upvoteCount',
|
||||
'downvoteCount',
|
||||
'indexedAt',
|
||||
'viewer',
|
||||
],
|
||||
properties: {
|
||||
uri: {
|
||||
type: 'string',
|
||||
},
|
||||
cid: {
|
||||
type: 'string',
|
||||
},
|
||||
author: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.actor.ref#withInfo',
|
||||
},
|
||||
record: {
|
||||
type: 'unknown',
|
||||
},
|
||||
embed: {
|
||||
type: 'union',
|
||||
refs: [
|
||||
'lex:app.bsky.embed.images#presented',
|
||||
'lex:app.bsky.embed.external#presented',
|
||||
],
|
||||
},
|
||||
replyCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
repostCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
upvoteCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
downvoteCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
indexedAt: {
|
||||
type: 'datetime',
|
||||
},
|
||||
viewer: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.feed.post#viewerState',
|
||||
},
|
||||
},
|
||||
},
|
||||
viewerState: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
repost: {
|
||||
type: 'string',
|
||||
},
|
||||
upvote: {
|
||||
type: 'string',
|
||||
},
|
||||
downvote: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AppBskyFeedRepost: {
|
||||
@ -3086,6 +3010,7 @@ export const ids = {
|
||||
AppBskyActorUpdateProfile: 'app.bsky.actor.updateProfile',
|
||||
AppBskyEmbedExternal: 'app.bsky.embed.external',
|
||||
AppBskyEmbedImages: 'app.bsky.embed.images',
|
||||
AppBskyFeedFeedViewPost: 'app.bsky.feed.feedViewPost',
|
||||
AppBskyFeedGetAuthorFeed: 'app.bsky.feed.getAuthorFeed',
|
||||
AppBskyFeedGetPostThread: 'app.bsky.feed.getPostThread',
|
||||
AppBskyFeedGetRepostedBy: 'app.bsky.feed.getRepostedBy',
|
||||
|
30
packages/api/src/client/types/app/bsky/feed/feedViewPost.ts
Normal file
30
packages/api/src/client/types/app/bsky/feed/feedViewPost.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* GENERATED CODE - DO NOT MODIFY
|
||||
*/
|
||||
import * as AppBskyFeedPost from './post'
|
||||
import * as AppBskyActorRef from '../actor/ref'
|
||||
|
||||
export interface Main {
|
||||
post: AppBskyFeedPost.View
|
||||
reply?: ReplyRef
|
||||
reason?: ReasonTrend | ReasonRepost | { $type: string; [k: string]: unknown }
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface ReplyRef {
|
||||
root: AppBskyFeedPost.View
|
||||
parent: AppBskyFeedPost.View
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface ReasonTrend {
|
||||
by: AppBskyActorRef.WithInfo
|
||||
indexedAt: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface ReasonRepost {
|
||||
by: AppBskyActorRef.WithInfo
|
||||
indexedAt: string
|
||||
[k: string]: unknown
|
||||
}
|
@ -2,9 +2,7 @@
|
||||
* GENERATED CODE - DO NOT MODIFY
|
||||
*/
|
||||
import { Headers, XRPCError } from '@atproto/xrpc'
|
||||
import * as AppBskyActorRef from '../actor/ref'
|
||||
import * as AppBskyEmbedImages from '../embed/images'
|
||||
import * as AppBskyEmbedExternal from '../embed/external'
|
||||
import * as AppBskyFeedFeedViewPost from './feedViewPost'
|
||||
|
||||
export interface QueryParams {
|
||||
author: string
|
||||
@ -16,7 +14,7 @@ export type InputSchema = undefined
|
||||
|
||||
export interface OutputSchema {
|
||||
cursor?: string
|
||||
feed: FeedItem[]
|
||||
feed: AppBskyFeedFeedViewPost.Main[]
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
@ -35,30 +33,3 @@ export function toKnownErr(e: any) {
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
export interface FeedItem {
|
||||
uri: string
|
||||
cid: string
|
||||
author: AppBskyActorRef.WithInfo
|
||||
trendedBy?: AppBskyActorRef.WithInfo
|
||||
repostedBy?: AppBskyActorRef.WithInfo
|
||||
record: {}
|
||||
embed?:
|
||||
| AppBskyEmbedImages.Presented
|
||||
| AppBskyEmbedExternal.Presented
|
||||
| { $type: string; [k: string]: unknown }
|
||||
replyCount: number
|
||||
repostCount: number
|
||||
upvoteCount: number
|
||||
downvoteCount: number
|
||||
indexedAt: string
|
||||
myState?: MyState
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface MyState {
|
||||
repost?: string
|
||||
upvote?: string
|
||||
downvote?: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
@ -2,9 +2,7 @@
|
||||
* GENERATED CODE - DO NOT MODIFY
|
||||
*/
|
||||
import { Headers, XRPCError } from '@atproto/xrpc'
|
||||
import * as AppBskyActorRef from '../actor/ref'
|
||||
import * as AppBskyEmbedImages from '../embed/images'
|
||||
import * as AppBskyEmbedExternal from '../embed/external'
|
||||
import * as AppBskyFeedPost from './post'
|
||||
|
||||
export interface QueryParams {
|
||||
uri: string
|
||||
@ -14,7 +12,10 @@ export interface QueryParams {
|
||||
export type InputSchema = undefined
|
||||
|
||||
export interface OutputSchema {
|
||||
thread: Post | NotFoundPost | { $type: string; [k: string]: unknown }
|
||||
thread:
|
||||
| ThreadViewPost
|
||||
| NotFoundPost
|
||||
| { $type: string; [k: string]: unknown }
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
@ -41,23 +42,17 @@ export function toKnownErr(e: any) {
|
||||
return e
|
||||
}
|
||||
|
||||
export interface Post {
|
||||
uri: string
|
||||
cid: string
|
||||
author: AppBskyActorRef.WithInfo
|
||||
record: {}
|
||||
embed?:
|
||||
| AppBskyEmbedImages.Presented
|
||||
| AppBskyEmbedExternal.Presented
|
||||
export interface ThreadViewPost {
|
||||
post: AppBskyFeedPost.View
|
||||
parent?:
|
||||
| ThreadViewPost
|
||||
| NotFoundPost
|
||||
| { $type: string; [k: string]: unknown }
|
||||
parent?: Post | NotFoundPost | { $type: string; [k: string]: unknown }
|
||||
replyCount: number
|
||||
replies?: (Post | NotFoundPost | { $type: string; [k: string]: unknown })[]
|
||||
repostCount: number
|
||||
upvoteCount: number
|
||||
downvoteCount: number
|
||||
indexedAt: string
|
||||
myState?: MyState
|
||||
replies?: (
|
||||
| ThreadViewPost
|
||||
| NotFoundPost
|
||||
| { $type: string; [k: string]: unknown }
|
||||
)[]
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
@ -66,10 +61,3 @@ export interface NotFoundPost {
|
||||
notFound: true
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface MyState {
|
||||
repost?: string
|
||||
upvote?: string
|
||||
downvote?: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
@ -2,9 +2,7 @@
|
||||
* GENERATED CODE - DO NOT MODIFY
|
||||
*/
|
||||
import { Headers, XRPCError } from '@atproto/xrpc'
|
||||
import * as AppBskyActorRef from '../actor/ref'
|
||||
import * as AppBskyEmbedImages from '../embed/images'
|
||||
import * as AppBskyEmbedExternal from '../embed/external'
|
||||
import * as AppBskyFeedFeedViewPost from './feedViewPost'
|
||||
|
||||
export interface QueryParams {
|
||||
algorithm?: string
|
||||
@ -16,7 +14,7 @@ export type InputSchema = undefined
|
||||
|
||||
export interface OutputSchema {
|
||||
cursor?: string
|
||||
feed: FeedItem[]
|
||||
feed: AppBskyFeedFeedViewPost.Main[]
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
@ -35,30 +33,3 @@ export function toKnownErr(e: any) {
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
export interface FeedItem {
|
||||
uri: string
|
||||
cid: string
|
||||
author: AppBskyActorRef.WithInfo
|
||||
trendedBy?: AppBskyActorRef.WithInfo
|
||||
repostedBy?: AppBskyActorRef.WithInfo
|
||||
record: {}
|
||||
embed?:
|
||||
| AppBskyEmbedImages.Presented
|
||||
| AppBskyEmbedExternal.Presented
|
||||
| { $type: string; [k: string]: unknown }
|
||||
replyCount: number
|
||||
repostCount: number
|
||||
upvoteCount: number
|
||||
downvoteCount: number
|
||||
indexedAt: string
|
||||
myState?: MyState
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface MyState {
|
||||
repost?: string
|
||||
upvote?: string
|
||||
downvote?: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
import * as AppBskyEmbedImages from '../embed/images'
|
||||
import * as AppBskyEmbedExternal from '../embed/external'
|
||||
import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef'
|
||||
import * as AppBskyActorRef from '../actor/ref'
|
||||
|
||||
export interface Record {
|
||||
text: string
|
||||
@ -36,3 +37,28 @@ export interface TextSlice {
|
||||
end: number
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface View {
|
||||
uri: string
|
||||
cid: string
|
||||
author: AppBskyActorRef.WithInfo
|
||||
record: {}
|
||||
embed?:
|
||||
| AppBskyEmbedImages.Presented
|
||||
| AppBskyEmbedExternal.Presented
|
||||
| { $type: string; [k: string]: unknown }
|
||||
replyCount: number
|
||||
repostCount: number
|
||||
upvoteCount: number
|
||||
downvoteCount: number
|
||||
indexedAt: string
|
||||
viewer: ViewerState
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface ViewerState {
|
||||
repost?: string
|
||||
upvote?: string
|
||||
downvote?: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { sql } from 'kysely'
|
||||
import { Server } from '../../../../lexicon'
|
||||
import { FeedItemType, FeedKeyset, composeFeed } from '../util/feed'
|
||||
import { countAll } from '../../../../db/util'
|
||||
import { FeedKeyset, composeFeed } from '../util/feed'
|
||||
import { paginate } from '../../../../db/pagination'
|
||||
import AppContext from '../../../../context'
|
||||
import { FeedRow } from '../../../../services/feed'
|
||||
|
||||
export default function (server: Server, ctx: AppContext) {
|
||||
server.app.bsky.feed.getAuthorFeed({
|
||||
@ -14,6 +13,8 @@ export default function (server: Server, ctx: AppContext) {
|
||||
const db = ctx.db.db
|
||||
const { ref } = db.dynamic
|
||||
|
||||
const feedService = ctx.services.feed(ctx.db)
|
||||
|
||||
const userLookupCol = author.startsWith('did:')
|
||||
? 'did_handle.did'
|
||||
: 'did_handle.handle'
|
||||
@ -22,145 +23,42 @@ export default function (server: Server, ctx: AppContext) {
|
||||
.selectAll()
|
||||
.where(userLookupCol, '=', author)
|
||||
|
||||
const postsQb = db
|
||||
.selectFrom('post')
|
||||
const postsQb = feedService
|
||||
.selectPostQb()
|
||||
.whereExists(
|
||||
userQb.whereRef('did_handle.did', '=', ref('post.creator')),
|
||||
)
|
||||
.select([
|
||||
sql<FeedItemType>`${'post'}`.as('type'),
|
||||
'uri as postUri',
|
||||
'cid as postCid',
|
||||
'creator as originatorDid',
|
||||
'indexedAt as cursor',
|
||||
])
|
||||
|
||||
const repostsQb = db
|
||||
.selectFrom('repost')
|
||||
const repostsQb = feedService
|
||||
.selectRepostQb()
|
||||
.whereExists(
|
||||
userQb.whereRef('did_handle.did', '=', ref('repost.creator')),
|
||||
)
|
||||
.select([
|
||||
sql<FeedItemType>`${'repost'}`.as('type'),
|
||||
'subject as postUri',
|
||||
'subjectCid as postCid',
|
||||
'creator as originatorDid',
|
||||
'indexedAt as cursor',
|
||||
])
|
||||
|
||||
const trendsQb = db
|
||||
.selectFrom('trend')
|
||||
const trendsQb = feedService
|
||||
.selectTrendQb()
|
||||
.whereExists(
|
||||
userQb.whereRef('did_handle.did', '=', ref('trend.creator')),
|
||||
)
|
||||
.select([
|
||||
sql<FeedItemType>`${'trend'}`.as('type'),
|
||||
'subject as postUri',
|
||||
'subjectCid as postCid',
|
||||
'creator as originatorDid',
|
||||
'indexedAt as cursor',
|
||||
])
|
||||
|
||||
let feedItemsQb = db
|
||||
.selectFrom(postsQb.union(repostsQb).union(trendsQb).as('feed_items'))
|
||||
.innerJoin('post', 'post.uri', 'postUri')
|
||||
.innerJoin('ipld_block', 'ipld_block.cid', 'post.cid')
|
||||
.innerJoin('did_handle as author', 'author.did', 'post.creator')
|
||||
.leftJoin(
|
||||
'profile as author_profile',
|
||||
'author_profile.creator',
|
||||
'author.did',
|
||||
)
|
||||
.innerJoin(
|
||||
'did_handle as originator',
|
||||
'originator.did',
|
||||
'originatorDid',
|
||||
)
|
||||
.leftJoin(
|
||||
'profile as originator_profile',
|
||||
'originator_profile.creator',
|
||||
'originatorDid',
|
||||
)
|
||||
.select([
|
||||
'type',
|
||||
'postUri',
|
||||
'postCid',
|
||||
'cursor',
|
||||
'ipld_block.content as recordBytes',
|
||||
'ipld_block.indexedAt as indexedAt',
|
||||
'author.did as authorDid',
|
||||
'author.declarationCid as authorDeclarationCid',
|
||||
'author.actorType as authorActorType',
|
||||
'author.handle as authorHandle',
|
||||
'author.actorType as authorActorType',
|
||||
'author_profile.displayName as authorDisplayName',
|
||||
'author_profile.avatarCid as authorAvatarCid',
|
||||
'originator.did as originatorDid',
|
||||
'originator.declarationCid as originatorDeclarationCid',
|
||||
'originator.actorType as originatorActorType',
|
||||
'originator.handle as originatorHandle',
|
||||
'originator.actorType as originatorActorType',
|
||||
'originator_profile.displayName as originatorDisplayName',
|
||||
'originator_profile.avatarCid as originatorAvatarCid',
|
||||
db
|
||||
.selectFrom('vote')
|
||||
.whereRef('subject', '=', ref('postUri'))
|
||||
.where('direction', '=', 'up')
|
||||
.select(countAll.as('count'))
|
||||
.as('upvoteCount'),
|
||||
db
|
||||
.selectFrom('vote')
|
||||
.whereRef('subject', '=', ref('postUri'))
|
||||
.where('direction', '=', 'down')
|
||||
.select(countAll.as('count'))
|
||||
.as('downvoteCount'),
|
||||
db
|
||||
.selectFrom('repost')
|
||||
.whereRef('subject', '=', ref('postUri'))
|
||||
.select(countAll.as('count'))
|
||||
.as('repostCount'),
|
||||
db
|
||||
.selectFrom('post')
|
||||
.whereRef('replyParent', '=', ref('postUri'))
|
||||
.select(countAll.as('count'))
|
||||
.as('replyCount'),
|
||||
db
|
||||
.selectFrom('repost')
|
||||
.where('creator', '=', requester)
|
||||
.whereRef('subject', '=', ref('postUri'))
|
||||
.select('uri')
|
||||
.as('requesterRepost'),
|
||||
db
|
||||
.selectFrom('vote')
|
||||
.where('creator', '=', requester)
|
||||
.whereRef('subject', '=', ref('postUri'))
|
||||
.where('direction', '=', 'up')
|
||||
.select('uri')
|
||||
.as('requesterUpvote'),
|
||||
db
|
||||
.selectFrom('vote')
|
||||
.where('creator', '=', requester)
|
||||
.whereRef('subject', '=', ref('postUri'))
|
||||
.where('direction', '=', 'down')
|
||||
.select('uri')
|
||||
.as('requesterDownvote'),
|
||||
])
|
||||
|
||||
const keyset = new FeedKeyset(ref('cursor'), ref('postCid'))
|
||||
let feedItemsQb = db
|
||||
.selectFrom(postsQb.union(repostsQb).union(trendsQb).as('feed_items'))
|
||||
.selectAll()
|
||||
feedItemsQb = paginate(feedItemsQb, {
|
||||
limit,
|
||||
before,
|
||||
keyset,
|
||||
})
|
||||
|
||||
const queryRes = await feedItemsQb.execute()
|
||||
const feed = await composeFeed(db, ctx.imgUriBuilder, queryRes)
|
||||
const feedItems: FeedRow[] = await feedItemsQb.execute()
|
||||
const feed = await composeFeed(feedService, feedItems, requester)
|
||||
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: {
|
||||
feed,
|
||||
cursor: keyset.packFromResult(queryRes),
|
||||
cursor: keyset.packFromResult(feedItems),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -1,13 +1,19 @@
|
||||
import { InvalidRequestError } from '@atproto/xrpc-server'
|
||||
import * as common from '@atproto/common'
|
||||
import { Server } from '../../../../lexicon'
|
||||
import * as GetPostThread from '../../../../lexicon/types/app/bsky/feed/getPostThread'
|
||||
import DatabaseSchema from '../../../../db/database-schema'
|
||||
import { countAll } from '../../../../db/util'
|
||||
import { getDeclaration } from '../util'
|
||||
import { ImageUriBuilder } from '../../../../image/uri'
|
||||
import { embedsForPosts, FeedEmbeds } from '../util/embeds'
|
||||
import AppContext from '../../../../context'
|
||||
import {
|
||||
ActorViewMap,
|
||||
FeedEmbeds,
|
||||
FeedRow,
|
||||
FeedService,
|
||||
PostInfoMap,
|
||||
} from '../../../../services/feed'
|
||||
import { InvalidRequestError } from '@atproto/xrpc-server'
|
||||
|
||||
export type PostThread = {
|
||||
post: FeedRow
|
||||
parent?: PostThread | ParentNotFoundError
|
||||
replies?: PostThread[]
|
||||
}
|
||||
|
||||
export default function (server: Server, ctx: AppContext) {
|
||||
server.app.bsky.feed.getPostThread({
|
||||
@ -15,36 +21,27 @@ export default function (server: Server, ctx: AppContext) {
|
||||
handler: async ({ params, auth }) => {
|
||||
const { uri, depth = 6 } = params
|
||||
const requester = auth.credentials.did
|
||||
const { db, imgUriBuilder } = ctx
|
||||
|
||||
const queryRes = await postInfoBuilder(db.db, requester)
|
||||
.where('post.uri', '=', uri)
|
||||
.executeTakeFirst()
|
||||
const feedService = ctx.services.feed(ctx.db)
|
||||
|
||||
if (!queryRes) {
|
||||
const threadData = await getThreadData(feedService, uri, depth)
|
||||
if (!threadData) {
|
||||
throw new InvalidRequestError(`Post not found: ${uri}`, 'NotFound')
|
||||
}
|
||||
const relevant = getRelevantIds(threadData)
|
||||
const [actors, posts, embeds] = await Promise.all([
|
||||
feedService.getActorViews(Array.from(relevant.dids)),
|
||||
feedService.getPostViews(Array.from(relevant.uris), requester),
|
||||
feedService.embedsForPosts(Array.from(relevant.uris)),
|
||||
])
|
||||
|
||||
const embeds = await embedsForPosts(db.db, imgUriBuilder, [queryRes.uri])
|
||||
const thread = rowToPost(imgUriBuilder, embeds, queryRes)
|
||||
if (depth > 0) {
|
||||
thread.replies = await getReplies(
|
||||
db.db,
|
||||
imgUriBuilder,
|
||||
thread,
|
||||
depth - 1,
|
||||
requester,
|
||||
)
|
||||
}
|
||||
if (queryRes.parent !== null) {
|
||||
thread.parent = await getAncestors(
|
||||
db.db,
|
||||
imgUriBuilder,
|
||||
queryRes.parent,
|
||||
requester,
|
||||
)
|
||||
}
|
||||
|
||||
const thread = composeThread(
|
||||
threadData,
|
||||
feedService,
|
||||
posts,
|
||||
actors,
|
||||
embeds,
|
||||
)
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: { thread },
|
||||
@ -53,169 +50,134 @@ export default function (server: Server, ctx: AppContext) {
|
||||
})
|
||||
}
|
||||
|
||||
const getAncestors = async (
|
||||
db: DatabaseSchema,
|
||||
imgUriBuilder: ImageUriBuilder,
|
||||
parentUri: string,
|
||||
requester: string,
|
||||
): Promise<GetPostThread.Post | GetPostThread.NotFoundPost> => {
|
||||
const parentRes = await postInfoBuilder(db, requester)
|
||||
.where('post.uri', '=', parentUri)
|
||||
.executeTakeFirst()
|
||||
if (!parentRes) {
|
||||
return {
|
||||
$type: 'app.bsky.feed.getPostThread#notFoundPost',
|
||||
uri: parentUri,
|
||||
notFound: true,
|
||||
const composeThread = (
|
||||
threadData: PostThread,
|
||||
feedService: FeedService,
|
||||
posts: PostInfoMap,
|
||||
actors: ActorViewMap,
|
||||
embeds: FeedEmbeds,
|
||||
) => {
|
||||
const post = feedService.formatPostView(
|
||||
threadData.post.postUri,
|
||||
actors,
|
||||
posts,
|
||||
embeds,
|
||||
)
|
||||
|
||||
let parent
|
||||
if (threadData.parent) {
|
||||
if (threadData.parent instanceof ParentNotFoundError) {
|
||||
parent = {
|
||||
$type: 'app.bsky.feed.getPostThread#notFoundPost',
|
||||
uri: threadData.parent.uri,
|
||||
notFound: true,
|
||||
}
|
||||
} else {
|
||||
parent = composeThread(
|
||||
threadData.parent,
|
||||
feedService,
|
||||
posts,
|
||||
actors,
|
||||
embeds,
|
||||
)
|
||||
}
|
||||
}
|
||||
const embeds = await embedsForPosts(db, imgUriBuilder, [parentRes.uri])
|
||||
const parentObj = rowToPost(imgUriBuilder, embeds, parentRes)
|
||||
if (parentRes.parent !== null) {
|
||||
parentObj.parent = await getAncestors(
|
||||
db,
|
||||
imgUriBuilder,
|
||||
parentRes.parent,
|
||||
requester,
|
||||
|
||||
let replies
|
||||
if (threadData.replies) {
|
||||
replies = threadData.replies.map((reply) =>
|
||||
composeThread(reply, feedService, posts, actors, embeds),
|
||||
)
|
||||
}
|
||||
return parentObj
|
||||
|
||||
return {
|
||||
$type: 'app.bksy.feed.getPostThread#threadViewPost',
|
||||
post,
|
||||
parent,
|
||||
replies,
|
||||
}
|
||||
}
|
||||
|
||||
const getReplies = async (
|
||||
db: DatabaseSchema,
|
||||
imgUriBuilder: ImageUriBuilder,
|
||||
parent: GetPostThread.Post,
|
||||
const getRelevantIds = (
|
||||
thread: PostThread,
|
||||
): { dids: Set<string>; uris: Set<string> } => {
|
||||
const dids = new Set<string>()
|
||||
const uris = new Set<string>()
|
||||
if (thread.parent && !(thread.parent instanceof ParentNotFoundError)) {
|
||||
const fromParent = getRelevantIds(thread.parent)
|
||||
fromParent.dids.forEach((did) => dids.add(did))
|
||||
fromParent.uris.forEach((uri) => uris.add(uri))
|
||||
}
|
||||
if (thread.replies) {
|
||||
for (const reply of thread.replies) {
|
||||
const fromChild = getRelevantIds(reply)
|
||||
fromChild.dids.forEach((did) => dids.add(did))
|
||||
fromChild.uris.forEach((uri) => uris.add(uri))
|
||||
}
|
||||
}
|
||||
dids.add(thread.post.authorDid)
|
||||
uris.add(thread.post.postUri)
|
||||
return { dids, uris }
|
||||
}
|
||||
|
||||
const getThreadData = async (
|
||||
feedService: FeedService,
|
||||
uri: string,
|
||||
depth: number,
|
||||
requester: string,
|
||||
): Promise<GetPostThread.Post[]> => {
|
||||
const res = await postInfoBuilder(db, requester)
|
||||
.where('post.replyParent', '=', parent.uri)
|
||||
): Promise<PostThread | null> => {
|
||||
const post = await feedService
|
||||
.selectPostQb()
|
||||
.where('post.uri', '=', uri)
|
||||
.executeTakeFirst()
|
||||
if (!post) return null
|
||||
return {
|
||||
post,
|
||||
parent: post.replyParent
|
||||
? await getParentData(feedService, post.replyParent)
|
||||
: undefined,
|
||||
replies: await getChildrenData(feedService, uri, depth),
|
||||
}
|
||||
}
|
||||
|
||||
const getParentData = async (
|
||||
feedService: FeedService,
|
||||
uri: string,
|
||||
): Promise<PostThread | ParentNotFoundError> => {
|
||||
const post = await feedService
|
||||
.selectPostQb()
|
||||
.where('post.uri', '=', uri)
|
||||
.executeTakeFirst()
|
||||
if (!post) return new ParentNotFoundError(uri)
|
||||
return {
|
||||
post,
|
||||
parent: post.replyParent
|
||||
? await getParentData(feedService, post.replyParent)
|
||||
: undefined,
|
||||
replies: [],
|
||||
}
|
||||
}
|
||||
|
||||
const getChildrenData = async (
|
||||
feedService: FeedService,
|
||||
uri: string,
|
||||
depth: number,
|
||||
): Promise<PostThread[] | undefined> => {
|
||||
if (depth === 0) return undefined
|
||||
const children = await feedService
|
||||
.selectPostQb()
|
||||
.where('post.replyParent', '=', uri)
|
||||
.orderBy('post.createdAt', 'desc')
|
||||
.execute()
|
||||
const postUris = res.map((row) => row.uri)
|
||||
const embeds = await embedsForPosts(db, imgUriBuilder, postUris)
|
||||
const got = await Promise.all(
|
||||
res.map(async (row) => {
|
||||
const post = rowToPost(imgUriBuilder, embeds, row, parent)
|
||||
if (depth > 0) {
|
||||
post.replies = await getReplies(
|
||||
db,
|
||||
imgUriBuilder,
|
||||
post,
|
||||
depth - 1,
|
||||
requester,
|
||||
)
|
||||
}
|
||||
return post
|
||||
}),
|
||||
return Promise.all(
|
||||
children.map(async (row) => ({
|
||||
post: row,
|
||||
replies: await getChildrenData(feedService, row.postUri, depth - 1),
|
||||
})),
|
||||
)
|
||||
return got
|
||||
}
|
||||
|
||||
// selects all the needed info about a post, just need to supply the `where` clause
|
||||
// @TODO break this query up, share parts with home/author feeds
|
||||
const postInfoBuilder = (db: DatabaseSchema, requester: string) => {
|
||||
const { ref } = db.dynamic
|
||||
return db
|
||||
.selectFrom('post')
|
||||
.innerJoin('ipld_block', 'ipld_block.cid', 'post.cid')
|
||||
.innerJoin('did_handle as author', 'author.did', 'post.creator')
|
||||
.leftJoin(
|
||||
'profile as author_profile',
|
||||
'author.did',
|
||||
'author_profile.creator',
|
||||
)
|
||||
.select([
|
||||
'post.uri as uri',
|
||||
'post.cid as cid',
|
||||
'post.replyParent as parent',
|
||||
'author.did as authorDid',
|
||||
'author.declarationCid as authorDeclarationCid',
|
||||
'author.actorType as authorActorType',
|
||||
'author.handle as authorHandle',
|
||||
'author_profile.displayName as authorDisplayName',
|
||||
'author_profile.avatarCid as authorAvatarCid',
|
||||
'ipld_block.content as recordBytes',
|
||||
'ipld_block.indexedAt as indexedAt',
|
||||
db
|
||||
.selectFrom('vote')
|
||||
.whereRef('subject', '=', ref('post.uri'))
|
||||
.where('direction', '=', 'up')
|
||||
.select(countAll.as('count'))
|
||||
.as('upvoteCount'),
|
||||
db
|
||||
.selectFrom('vote')
|
||||
.whereRef('subject', '=', ref('post.uri'))
|
||||
.where('direction', '=', 'down')
|
||||
.select(countAll.as('count'))
|
||||
.as('downvoteCount'),
|
||||
db
|
||||
.selectFrom('repost')
|
||||
.select(countAll.as('count'))
|
||||
.whereRef('subject', '=', ref('post.uri'))
|
||||
.as('repostCount'),
|
||||
db
|
||||
.selectFrom('post as reply')
|
||||
.select(countAll.as('count'))
|
||||
.whereRef('replyParent', '=', ref('post.uri'))
|
||||
.as('replyCount'),
|
||||
db
|
||||
.selectFrom('repost')
|
||||
.select('uri')
|
||||
.where('creator', '=', requester)
|
||||
.whereRef('subject', '=', ref('post.uri'))
|
||||
.as('requesterRepost'),
|
||||
db
|
||||
.selectFrom('vote')
|
||||
.where('creator', '=', requester)
|
||||
.whereRef('subject', '=', ref('post.uri'))
|
||||
.where('direction', '=', 'up')
|
||||
.select('uri')
|
||||
.as('requesterUpvote'),
|
||||
db
|
||||
.selectFrom('vote')
|
||||
.where('creator', '=', requester)
|
||||
.whereRef('subject', '=', ref('post.uri'))
|
||||
.where('direction', '=', 'down')
|
||||
.select('uri')
|
||||
.as('requesterDownvote'),
|
||||
])
|
||||
}
|
||||
|
||||
// converts the raw SQL output to a Post object
|
||||
// unfortunately not type-checked yet, so change with caution!
|
||||
const rowToPost = (
|
||||
imgUriBuilder: ImageUriBuilder,
|
||||
embeds: FeedEmbeds,
|
||||
row: any,
|
||||
parent?: GetPostThread.Post,
|
||||
): GetPostThread.Post => {
|
||||
return {
|
||||
$type: 'app.bsky.feed.getPostThread#post',
|
||||
uri: row.uri,
|
||||
cid: row.cid,
|
||||
author: {
|
||||
did: row.authorDid,
|
||||
declaration: getDeclaration('author', row),
|
||||
handle: row.authorHandle,
|
||||
displayName: row.authorDisplayName || undefined,
|
||||
avatar: row.authorAvatarCid
|
||||
? imgUriBuilder.getCommonSignedUri('avatar', row.authorAvatarCid)
|
||||
: undefined,
|
||||
},
|
||||
record: common.ipldBytesToRecord(row.recordBytes),
|
||||
embed: embeds[row.uri],
|
||||
parent: parent ? { ...parent } : undefined,
|
||||
replyCount: row.replyCount || 0,
|
||||
upvoteCount: row.upvoteCount || 0,
|
||||
downvoteCount: row.downvoteCount || 0,
|
||||
repostCount: row.repostCount || 0,
|
||||
indexedAt: row.indexedAt,
|
||||
myState: {
|
||||
repost: row.requesterRepost || undefined,
|
||||
upvote: row.requesterUpvote || undefined,
|
||||
downvote: row.requesterDownvote || undefined,
|
||||
},
|
||||
class ParentNotFoundError extends Error {
|
||||
constructor(public uri: string) {
|
||||
super(`Parent not found: ${uri}`)
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,9 @@
|
||||
import { sql } from 'kysely'
|
||||
import { InvalidRequestError } from '@atproto/xrpc-server'
|
||||
import { Server } from '../../../../lexicon'
|
||||
import { isEnum } from '../util'
|
||||
import {
|
||||
FeedAlgorithm,
|
||||
FeedItemType,
|
||||
FeedKeyset,
|
||||
composeFeed,
|
||||
} from '../util/feed'
|
||||
import { countAll } from '../../../../db/util'
|
||||
import { FeedAlgorithm, FeedKeyset, composeFeed } from '../util/feed'
|
||||
import { paginate } from '../../../../db/pagination'
|
||||
import AppContext from '../../../../context'
|
||||
import { FeedRow } from '../../../../services/feed'
|
||||
|
||||
export default function (server: Server, ctx: AppContext) {
|
||||
server.app.bsky.feed.getTimeline({
|
||||
@ -21,167 +14,49 @@ export default function (server: Server, ctx: AppContext) {
|
||||
const { ref } = db.dynamic
|
||||
const requester = auth.credentials.did
|
||||
|
||||
let feedAlgorithm: FeedAlgorithm
|
||||
if (isEnum(FeedAlgorithm, algorithm)) {
|
||||
feedAlgorithm = algorithm
|
||||
} else if (algorithm === undefined) {
|
||||
feedAlgorithm = FeedAlgorithm.ReverseChronological
|
||||
} else {
|
||||
if (algorithm && algorithm !== FeedAlgorithm.ReverseChronological) {
|
||||
throw new InvalidRequestError(`Unsupported algorithm: ${algorithm}`)
|
||||
}
|
||||
|
||||
let postsQb = db
|
||||
.selectFrom('post')
|
||||
.select([
|
||||
sql<FeedItemType>`${'post'}`.as('type'),
|
||||
'uri as postUri',
|
||||
'cid as postCid',
|
||||
'creator as originatorDid',
|
||||
'indexedAt as cursor',
|
||||
])
|
||||
const feedService = ctx.services.feed(ctx.db)
|
||||
|
||||
let repostsQb = db
|
||||
.selectFrom('repost')
|
||||
.select([
|
||||
sql<FeedItemType>`${'repost'}`.as('type'),
|
||||
'subject as postUri',
|
||||
'subjectCid as postCid',
|
||||
'creator as originatorDid',
|
||||
'indexedAt as cursor',
|
||||
])
|
||||
const followingIdsSubquery = db
|
||||
.selectFrom('follow')
|
||||
.select('follow.subjectDid')
|
||||
.where('follow.creator', '=', requester)
|
||||
|
||||
let trendsQb = db
|
||||
.selectFrom('trend')
|
||||
.select([
|
||||
sql<FeedItemType>`${'trend'}`.as('type'),
|
||||
'subject as postUri',
|
||||
'subjectCid as postCid',
|
||||
'creator as originatorDid',
|
||||
'indexedAt as cursor',
|
||||
])
|
||||
const postsQb = feedService
|
||||
.selectPostQb()
|
||||
.where('creator', '=', requester)
|
||||
.orWhere('creator', 'in', followingIdsSubquery)
|
||||
|
||||
if (feedAlgorithm === FeedAlgorithm.Firehose) {
|
||||
// All posts
|
||||
} else if (feedAlgorithm === FeedAlgorithm.ReverseChronological) {
|
||||
// Followee's posts/reposts/trends, and requester's posts
|
||||
const followingIdsSubquery = db
|
||||
.selectFrom('follow')
|
||||
.select('follow.subjectDid')
|
||||
.where('follow.creator', '=', requester)
|
||||
repostsQb = repostsQb
|
||||
.where('creator', '=', requester)
|
||||
.orWhere('creator', 'in', followingIdsSubquery)
|
||||
trendsQb = trendsQb
|
||||
.where('creator', '=', requester)
|
||||
.orWhere('creator', 'in', followingIdsSubquery)
|
||||
postsQb = postsQb
|
||||
.where('creator', '=', requester)
|
||||
.orWhere('creator', 'in', followingIdsSubquery)
|
||||
} else {
|
||||
const exhaustiveCheck: never = feedAlgorithm
|
||||
throw new Error(`Unhandled case: ${exhaustiveCheck}`)
|
||||
}
|
||||
const repostsQb = feedService
|
||||
.selectRepostQb()
|
||||
.where('repost.creator', '=', requester)
|
||||
.orWhere('repost.creator', 'in', followingIdsSubquery)
|
||||
|
||||
let feedItemsQb = db
|
||||
.selectFrom(postsQb.union(repostsQb).union(trendsQb).as('feed_items'))
|
||||
.innerJoin('post', 'post.uri', 'postUri')
|
||||
.innerJoin('ipld_block', 'ipld_block.cid', 'post.cid')
|
||||
.innerJoin('did_handle as author', 'author.did', 'post.creator')
|
||||
.leftJoin(
|
||||
'profile as author_profile',
|
||||
'author_profile.creator',
|
||||
'author.did',
|
||||
)
|
||||
.innerJoin(
|
||||
'did_handle as originator',
|
||||
'originator.did',
|
||||
'originatorDid',
|
||||
)
|
||||
.leftJoin(
|
||||
'profile as originator_profile',
|
||||
'originator_profile.creator',
|
||||
'originatorDid',
|
||||
)
|
||||
.select([
|
||||
'type',
|
||||
'postUri',
|
||||
'postCid',
|
||||
'cursor',
|
||||
'ipld_block.content as recordBytes',
|
||||
'ipld_block.indexedAt as indexedAt',
|
||||
'author.did as authorDid',
|
||||
'author.declarationCid as authorDeclarationCid',
|
||||
'author.actorType as authorActorType',
|
||||
'author.handle as authorHandle',
|
||||
'author.actorType as authorActorType',
|
||||
'author_profile.displayName as authorDisplayName',
|
||||
'author_profile.avatarCid as authorAvatarCid',
|
||||
'originator.did as originatorDid',
|
||||
'originator.declarationCid as originatorDeclarationCid',
|
||||
'originator.actorType as originatorActorType',
|
||||
'originator.handle as originatorHandle',
|
||||
'originator.actorType as originatorActorType',
|
||||
'originator_profile.displayName as originatorDisplayName',
|
||||
'originator_profile.avatarCid as originatorAvatarCid',
|
||||
db
|
||||
.selectFrom('vote')
|
||||
.whereRef('subject', '=', ref('postUri'))
|
||||
.where('direction', '=', 'up')
|
||||
.select(countAll.as('count'))
|
||||
.as('upvoteCount'),
|
||||
db
|
||||
.selectFrom('vote')
|
||||
.whereRef('subject', '=', ref('postUri'))
|
||||
.where('direction', '=', 'down')
|
||||
.select(countAll.as('count'))
|
||||
.as('downvoteCount'),
|
||||
db
|
||||
.selectFrom('repost')
|
||||
.whereRef('subject', '=', ref('postUri'))
|
||||
.select(countAll.as('count'))
|
||||
.as('repostCount'),
|
||||
db
|
||||
.selectFrom('post')
|
||||
.whereRef('replyParent', '=', ref('postUri'))
|
||||
.select(countAll.as('count'))
|
||||
.as('replyCount'),
|
||||
db
|
||||
.selectFrom('repost')
|
||||
.where('creator', '=', requester)
|
||||
.whereRef('subject', '=', ref('postUri'))
|
||||
.select('uri')
|
||||
.as('requesterRepost'),
|
||||
db
|
||||
.selectFrom('vote')
|
||||
.where('creator', '=', requester)
|
||||
.whereRef('subject', '=', ref('postUri'))
|
||||
.where('direction', '=', 'up')
|
||||
.select('uri')
|
||||
.as('requesterUpvote'),
|
||||
db
|
||||
.selectFrom('vote')
|
||||
.where('creator', '=', requester)
|
||||
.whereRef('subject', '=', ref('postUri'))
|
||||
.where('direction', '=', 'down')
|
||||
.select('uri')
|
||||
.as('requesterDownvote'),
|
||||
])
|
||||
const trendsQb = feedService
|
||||
.selectTrendQb()
|
||||
.where('trend.creator', '=', requester)
|
||||
.orWhere('trend.creator', 'in', followingIdsSubquery)
|
||||
|
||||
const keyset = new FeedKeyset(ref('cursor'), ref('postCid'))
|
||||
let feedItemsQb = db
|
||||
.selectFrom(postsQb.union(repostsQb).union(trendsQb).as('feed_items'))
|
||||
.selectAll()
|
||||
feedItemsQb = paginate(feedItemsQb, {
|
||||
limit,
|
||||
before,
|
||||
keyset,
|
||||
})
|
||||
|
||||
const queryRes = await feedItemsQb.execute()
|
||||
const feed = await composeFeed(db, ctx.imgUriBuilder, queryRes)
|
||||
const feedItems: FeedRow[] = await feedItemsQb.execute()
|
||||
const feed = await composeFeed(feedService, feedItems, requester)
|
||||
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: {
|
||||
feed,
|
||||
cursor: keyset.packFromResult(queryRes),
|
||||
cursor: keyset.packFromResult(feedItems),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -1,61 +0,0 @@
|
||||
import { Presented as PresentedImage } from '../../../../lexicon/types/app/bsky/embed/images'
|
||||
import { Presented as PresentedExternal } from '../../../../lexicon/types/app/bsky/embed/external'
|
||||
import { ImageUriBuilder } from '../../../../image/uri'
|
||||
import DatabaseSchema from '../../../../db/database-schema'
|
||||
|
||||
export type FeedEmbeds = {
|
||||
[uri: string]: PresentedImage | PresentedExternal
|
||||
}
|
||||
|
||||
export const embedsForPosts = async (
|
||||
db: DatabaseSchema,
|
||||
imgUriBuilder: ImageUriBuilder,
|
||||
postUris: string[],
|
||||
): Promise<FeedEmbeds> => {
|
||||
if (postUris.length < 1) {
|
||||
return {}
|
||||
}
|
||||
const imgPromise = db
|
||||
.selectFrom('post_embed_image')
|
||||
.selectAll()
|
||||
.where('postUri', 'in', postUris)
|
||||
.orderBy('postUri')
|
||||
.orderBy('position')
|
||||
.execute()
|
||||
const extPromise = db
|
||||
.selectFrom('post_embed_external')
|
||||
.selectAll()
|
||||
.where('postUri', 'in', postUris)
|
||||
.execute()
|
||||
const [images, externals] = await Promise.all([imgPromise, extPromise])
|
||||
const imgEmbeds = images.reduce((acc, cur) => {
|
||||
if (!acc[cur.postUri]) {
|
||||
acc[cur.postUri] = {
|
||||
$type: 'app.bsky.embed.images#presented',
|
||||
images: [],
|
||||
}
|
||||
}
|
||||
acc[cur.postUri].images.push({
|
||||
thumb: imgUriBuilder.getCommonSignedUri('feed_thumbnail', cur.imageCid),
|
||||
fullsize: imgUriBuilder.getCommonSignedUri('feed_fullsize', cur.imageCid),
|
||||
alt: cur.alt,
|
||||
})
|
||||
return acc
|
||||
}, {} as { [uri: string]: PresentedImage })
|
||||
return externals.reduce((acc, cur) => {
|
||||
if (!acc[cur.postUri]) {
|
||||
acc[cur.postUri] = {
|
||||
$type: 'app.bsky.embed.external#presented',
|
||||
external: {
|
||||
uri: cur.uri,
|
||||
title: cur.title,
|
||||
description: cur.description,
|
||||
thumb: cur.thumbCid
|
||||
? imgUriBuilder.getCommonSignedUri('feed_thumbnail', cur.thumbCid)
|
||||
: undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, imgEmbeds as FeedEmbeds)
|
||||
}
|
@ -1,113 +1,80 @@
|
||||
import * as common from '@atproto/common'
|
||||
import { getDeclaration } from '.'
|
||||
import { AtUri } from '@atproto/uri'
|
||||
import { TimeCidKeyset } from '../../../../db/pagination'
|
||||
import * as GetAuthorFeed from '../../../../lexicon/types/app/bsky/feed/getAuthorFeed'
|
||||
import * as GetTimeline from '../../../../lexicon/types/app/bsky/feed/getTimeline'
|
||||
import { CID } from 'multiformats/cid'
|
||||
import { ImageUriBuilder } from '../../../../image/uri'
|
||||
import { embedsForPosts, FeedEmbeds } from './embeds'
|
||||
import DatabaseSchema from '../../../../db/database-schema'
|
||||
import { Main as FeedViewPost } from '../../../../lexicon/types/app/bsky/feed/feedViewPost'
|
||||
import { FeedRow, FeedService } from '../../../../services/feed'
|
||||
|
||||
// Present post and repost results into FeedItems
|
||||
// Present post and repost results into FeedViewPosts
|
||||
// Including links to embedded media
|
||||
export const composeFeed = async (
|
||||
db: DatabaseSchema,
|
||||
imgUriBuilder: ImageUriBuilder,
|
||||
feedService: FeedService,
|
||||
rows: FeedRow[],
|
||||
): Promise<FeedItem[]> => {
|
||||
const feedUris = rows.map((row) => row.postUri)
|
||||
const embeds = await embedsForPosts(db, imgUriBuilder, feedUris)
|
||||
return rows.map(rowToFeedItem(imgUriBuilder, embeds))
|
||||
}
|
||||
requester: string,
|
||||
): Promise<FeedViewPost[]> => {
|
||||
const actorDids = new Set<string>()
|
||||
const postUris = new Set<string>()
|
||||
for (const row of rows) {
|
||||
actorDids.add(row.originatorDid)
|
||||
actorDids.add(row.authorDid)
|
||||
postUris.add(row.postUri)
|
||||
if (row.replyParent) {
|
||||
postUris.add(row.replyParent)
|
||||
actorDids.add(new AtUri(row.replyParent).host)
|
||||
}
|
||||
if (row.replyRoot) {
|
||||
postUris.add(row.replyRoot)
|
||||
actorDids.add(new AtUri(row.replyRoot).host)
|
||||
}
|
||||
}
|
||||
const [actors, posts, embeds] = await Promise.all([
|
||||
feedService.getActorViews(Array.from(actorDids)),
|
||||
feedService.getPostViews(Array.from(postUris), requester),
|
||||
feedService.embedsForPosts(Array.from(postUris)),
|
||||
])
|
||||
|
||||
export const rowToFeedItem =
|
||||
(imgUriBuilder: ImageUriBuilder, embeds: FeedEmbeds) =>
|
||||
(row: FeedRow): FeedItem => ({
|
||||
uri: row.postUri,
|
||||
cid: row.postCid,
|
||||
author: rowToAuthor(imgUriBuilder, row),
|
||||
trendedBy:
|
||||
row.type === 'trend' ? rowToOriginator(imgUriBuilder, row) : undefined,
|
||||
repostedBy:
|
||||
row.type === 'repost' ? rowToOriginator(imgUriBuilder, row) : undefined,
|
||||
record: common.ipldBytesToRecord(row.recordBytes),
|
||||
embed: embeds[row.postUri],
|
||||
replyCount: row.replyCount,
|
||||
repostCount: row.repostCount,
|
||||
upvoteCount: row.upvoteCount,
|
||||
downvoteCount: row.downvoteCount,
|
||||
indexedAt: row.indexedAt,
|
||||
myState: {
|
||||
repost: row.requesterRepost ?? undefined,
|
||||
upvote: row.requesterUpvote ?? undefined,
|
||||
downvote: row.requesterDownvote ?? undefined,
|
||||
},
|
||||
})
|
||||
const feed: FeedViewPost[] = []
|
||||
for (const row of rows) {
|
||||
const post = feedService.formatPostView(row.postUri, actors, posts, embeds)
|
||||
const originator = actors[row.originatorDid]
|
||||
if (post && originator) {
|
||||
let reasonType: string | undefined
|
||||
if (row.type === 'trend') {
|
||||
reasonType = 'app.bsky.feed.feedViewPost#reasonTrend'
|
||||
} else if (row.type === 'repost') {
|
||||
reasonType = 'app.bsky.feed.feedViewPost#reasonRepost'
|
||||
}
|
||||
const replyParent = row.replyParent
|
||||
? feedService.formatPostView(row.replyParent, actors, posts, embeds)
|
||||
: undefined
|
||||
const replyRoot = row.replyRoot
|
||||
? feedService.formatPostView(row.replyRoot, actors, posts, embeds)
|
||||
: undefined
|
||||
|
||||
const rowToAuthor = (imgUriBuilder: ImageUriBuilder, row: FeedRow) => ({
|
||||
did: row.authorDid,
|
||||
declaration: getDeclaration('author', row),
|
||||
handle: row.authorHandle,
|
||||
displayName: row.authorDisplayName ?? undefined,
|
||||
avatar: row.authorAvatarCid
|
||||
? imgUriBuilder.getCommonSignedUri('avatar', row.authorAvatarCid)
|
||||
: undefined,
|
||||
})
|
||||
|
||||
const rowToOriginator = (imgUriBuilder: ImageUriBuilder, row: FeedRow) => ({
|
||||
did: row.originatorDid,
|
||||
declaration: getDeclaration('originator', row),
|
||||
handle: row.originatorHandle,
|
||||
displayName: row.originatorDisplayName ?? undefined,
|
||||
avatar: row.originatorAvatarCid
|
||||
? imgUriBuilder.getSignedUri({
|
||||
cid: CID.parse(row.originatorAvatarCid),
|
||||
format: 'jpeg',
|
||||
fit: 'cover',
|
||||
height: 250,
|
||||
width: 250,
|
||||
min: true,
|
||||
feed.push({
|
||||
post,
|
||||
reason: reasonType
|
||||
? {
|
||||
$type: reasonType,
|
||||
by: actors[row.originatorDid],
|
||||
indexedAt: row.cursor,
|
||||
}
|
||||
: undefined,
|
||||
reply:
|
||||
replyRoot && replyParent
|
||||
? {
|
||||
root: replyRoot,
|
||||
parent: replyParent,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
return feed
|
||||
}
|
||||
|
||||
export enum FeedAlgorithm {
|
||||
Firehose = 'firehose',
|
||||
ReverseChronological = 'reverse-chronological',
|
||||
}
|
||||
|
||||
type FeedItem = GetAuthorFeed.FeedItem & GetTimeline.FeedItem
|
||||
|
||||
export type FeedItemType = 'post' | 'repost' | 'trend'
|
||||
|
||||
type FeedRow = {
|
||||
type: FeedItemType
|
||||
postUri: string
|
||||
postCid: string
|
||||
cursor: string
|
||||
recordBytes: Uint8Array
|
||||
indexedAt: string
|
||||
authorDid: string
|
||||
authorDeclarationCid: string
|
||||
authorActorType: string
|
||||
authorHandle: string
|
||||
authorDisplayName: string | null
|
||||
authorAvatarCid: string | null
|
||||
originatorDid: string
|
||||
originatorDeclarationCid: string
|
||||
originatorActorType: string
|
||||
originatorHandle: string
|
||||
originatorDisplayName: string | null
|
||||
originatorAvatarCid: string | null
|
||||
upvoteCount: number
|
||||
downvoteCount: number
|
||||
repostCount: number
|
||||
replyCount: number
|
||||
requesterRepost: string | null
|
||||
requesterUpvote: string | null
|
||||
requesterDownvote: string | null
|
||||
}
|
||||
|
||||
export class FeedKeyset extends TimeCidKeyset<FeedRow> {
|
||||
labelResult(result: FeedRow) {
|
||||
return { primary: result.cursor, secondary: result.postCid }
|
||||
|
@ -62,8 +62,6 @@ export class PDS {
|
||||
const messageQueue = new SqlMessageQueue('pds', db)
|
||||
streamConsumers.listen(messageQueue, blobstore, auth, keypair)
|
||||
|
||||
const services = createServices(messageQueue, blobstore)
|
||||
|
||||
const mailTransport =
|
||||
config.emailSmtpUrl !== undefined
|
||||
? createTransport(config.emailSmtpUrl)
|
||||
@ -94,6 +92,8 @@ export class PDS {
|
||||
config.imgUriKey,
|
||||
)
|
||||
|
||||
const services = createServices({ messageQueue, blobstore, imgUriBuilder })
|
||||
|
||||
const ctx = new AppContext({
|
||||
db,
|
||||
blobstore,
|
||||
|
@ -1566,6 +1566,73 @@ export const schemaDict = {
|
||||
},
|
||||
},
|
||||
},
|
||||
AppBskyFeedFeedViewPost: {
|
||||
lexicon: 1,
|
||||
id: 'app.bsky.feed.feedViewPost',
|
||||
defs: {
|
||||
main: {
|
||||
type: 'object',
|
||||
required: ['post'],
|
||||
properties: {
|
||||
post: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.feed.post#view',
|
||||
},
|
||||
reply: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.feed.feedViewPost#replyRef',
|
||||
},
|
||||
reason: {
|
||||
type: 'union',
|
||||
refs: [
|
||||
'lex:app.bsky.feed.feedViewPost#reasonTrend',
|
||||
'lex:app.bsky.feed.feedViewPost#reasonRepost',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
replyRef: {
|
||||
type: 'object',
|
||||
required: ['root', 'parent'],
|
||||
properties: {
|
||||
root: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.feed.post#view',
|
||||
},
|
||||
parent: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.feed.post#view',
|
||||
},
|
||||
},
|
||||
},
|
||||
reasonTrend: {
|
||||
type: 'object',
|
||||
required: ['by', 'indexedAt'],
|
||||
properties: {
|
||||
by: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.actor.ref#withInfo',
|
||||
},
|
||||
indexedAt: {
|
||||
type: 'datetime',
|
||||
},
|
||||
},
|
||||
},
|
||||
reasonRepost: {
|
||||
type: 'object',
|
||||
required: ['by', 'indexedAt'],
|
||||
properties: {
|
||||
by: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.actor.ref#withInfo',
|
||||
},
|
||||
indexedAt: {
|
||||
type: 'datetime',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AppBskyFeedGetAuthorFeed: {
|
||||
lexicon: 1,
|
||||
id: 'app.bsky.feed.getAuthorFeed',
|
||||
@ -1604,90 +1671,13 @@ export const schemaDict = {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.feed.getAuthorFeed#feedItem',
|
||||
ref: 'lex:app.bsky.feed.feedViewPost',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
feedItem: {
|
||||
type: 'object',
|
||||
required: [
|
||||
'uri',
|
||||
'cid',
|
||||
'author',
|
||||
'record',
|
||||
'replyCount',
|
||||
'repostCount',
|
||||
'upvoteCount',
|
||||
'downvoteCount',
|
||||
'indexedAt',
|
||||
],
|
||||
properties: {
|
||||
uri: {
|
||||
type: 'string',
|
||||
},
|
||||
cid: {
|
||||
type: 'string',
|
||||
},
|
||||
author: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.actor.ref#withInfo',
|
||||
},
|
||||
trendedBy: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.actor.ref#withInfo',
|
||||
},
|
||||
repostedBy: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.actor.ref#withInfo',
|
||||
},
|
||||
record: {
|
||||
type: 'unknown',
|
||||
},
|
||||
embed: {
|
||||
type: 'union',
|
||||
refs: [
|
||||
'lex:app.bsky.embed.images#presented',
|
||||
'lex:app.bsky.embed.external#presented',
|
||||
],
|
||||
},
|
||||
replyCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
repostCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
upvoteCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
downvoteCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
indexedAt: {
|
||||
type: 'datetime',
|
||||
},
|
||||
myState: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.feed.getAuthorFeed#myState',
|
||||
},
|
||||
},
|
||||
},
|
||||
myState: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
repost: {
|
||||
type: 'string',
|
||||
},
|
||||
upvote: {
|
||||
type: 'string',
|
||||
},
|
||||
downvote: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AppBskyFeedGetPostThread: {
|
||||
@ -1717,7 +1707,7 @@ export const schemaDict = {
|
||||
thread: {
|
||||
type: 'union',
|
||||
refs: [
|
||||
'lex:app.bsky.feed.getPostThread#post',
|
||||
'lex:app.bsky.feed.getPostThread#threadViewPost',
|
||||
'lex:app.bsky.feed.getPostThread#notFoundPost',
|
||||
],
|
||||
},
|
||||
@ -1730,76 +1720,31 @@ export const schemaDict = {
|
||||
},
|
||||
],
|
||||
},
|
||||
post: {
|
||||
threadViewPost: {
|
||||
type: 'object',
|
||||
required: [
|
||||
'uri',
|
||||
'cid',
|
||||
'author',
|
||||
'record',
|
||||
'replyCount',
|
||||
'repostCount',
|
||||
'upvoteCount',
|
||||
'downvoteCount',
|
||||
'indexedAt',
|
||||
],
|
||||
required: ['post'],
|
||||
properties: {
|
||||
uri: {
|
||||
type: 'string',
|
||||
},
|
||||
cid: {
|
||||
type: 'string',
|
||||
},
|
||||
author: {
|
||||
post: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.actor.ref#withInfo',
|
||||
},
|
||||
record: {
|
||||
type: 'unknown',
|
||||
},
|
||||
embed: {
|
||||
type: 'union',
|
||||
refs: [
|
||||
'lex:app.bsky.embed.images#presented',
|
||||
'lex:app.bsky.embed.external#presented',
|
||||
],
|
||||
ref: 'lex:app.bsky.feed.post#view',
|
||||
},
|
||||
parent: {
|
||||
type: 'union',
|
||||
refs: [
|
||||
'lex:app.bsky.feed.getPostThread#post',
|
||||
'lex:app.bsky.feed.getPostThread#threadViewPost',
|
||||
'lex:app.bsky.feed.getPostThread#notFoundPost',
|
||||
],
|
||||
},
|
||||
replyCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
replies: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'union',
|
||||
refs: [
|
||||
'lex:app.bsky.feed.getPostThread#post',
|
||||
'lex:app.bsky.feed.getPostThread#threadViewPost',
|
||||
'lex:app.bsky.feed.getPostThread#notFoundPost',
|
||||
],
|
||||
},
|
||||
},
|
||||
repostCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
upvoteCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
downvoteCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
indexedAt: {
|
||||
type: 'datetime',
|
||||
},
|
||||
myState: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.feed.getPostThread#myState',
|
||||
},
|
||||
},
|
||||
},
|
||||
notFoundPost: {
|
||||
@ -1815,20 +1760,6 @@ export const schemaDict = {
|
||||
},
|
||||
},
|
||||
},
|
||||
myState: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
repost: {
|
||||
type: 'string',
|
||||
},
|
||||
upvote: {
|
||||
type: 'string',
|
||||
},
|
||||
downvote: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AppBskyFeedGetRepostedBy: {
|
||||
@ -1952,90 +1883,13 @@ export const schemaDict = {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.feed.getTimeline#feedItem',
|
||||
ref: 'lex:app.bsky.feed.feedViewPost',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
feedItem: {
|
||||
type: 'object',
|
||||
required: [
|
||||
'uri',
|
||||
'cid',
|
||||
'author',
|
||||
'record',
|
||||
'replyCount',
|
||||
'repostCount',
|
||||
'upvoteCount',
|
||||
'downvoteCount',
|
||||
'indexedAt',
|
||||
],
|
||||
properties: {
|
||||
uri: {
|
||||
type: 'string',
|
||||
},
|
||||
cid: {
|
||||
type: 'string',
|
||||
},
|
||||
author: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.actor.ref#withInfo',
|
||||
},
|
||||
trendedBy: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.actor.ref#withInfo',
|
||||
},
|
||||
repostedBy: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.actor.ref#withInfo',
|
||||
},
|
||||
record: {
|
||||
type: 'unknown',
|
||||
},
|
||||
embed: {
|
||||
type: 'union',
|
||||
refs: [
|
||||
'lex:app.bsky.embed.images#presented',
|
||||
'lex:app.bsky.embed.external#presented',
|
||||
],
|
||||
},
|
||||
replyCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
repostCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
upvoteCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
downvoteCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
indexedAt: {
|
||||
type: 'datetime',
|
||||
},
|
||||
myState: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.feed.getTimeline#myState',
|
||||
},
|
||||
},
|
||||
},
|
||||
myState: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
repost: {
|
||||
type: 'string',
|
||||
},
|
||||
upvote: {
|
||||
type: 'string',
|
||||
},
|
||||
downvote: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AppBskyFeedGetVotes: {
|
||||
@ -2202,6 +2056,76 @@ export const schemaDict = {
|
||||
},
|
||||
},
|
||||
},
|
||||
view: {
|
||||
type: 'object',
|
||||
required: [
|
||||
'uri',
|
||||
'cid',
|
||||
'author',
|
||||
'record',
|
||||
'replyCount',
|
||||
'repostCount',
|
||||
'upvoteCount',
|
||||
'downvoteCount',
|
||||
'indexedAt',
|
||||
'viewer',
|
||||
],
|
||||
properties: {
|
||||
uri: {
|
||||
type: 'string',
|
||||
},
|
||||
cid: {
|
||||
type: 'string',
|
||||
},
|
||||
author: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.actor.ref#withInfo',
|
||||
},
|
||||
record: {
|
||||
type: 'unknown',
|
||||
},
|
||||
embed: {
|
||||
type: 'union',
|
||||
refs: [
|
||||
'lex:app.bsky.embed.images#presented',
|
||||
'lex:app.bsky.embed.external#presented',
|
||||
],
|
||||
},
|
||||
replyCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
repostCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
upvoteCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
downvoteCount: {
|
||||
type: 'integer',
|
||||
},
|
||||
indexedAt: {
|
||||
type: 'datetime',
|
||||
},
|
||||
viewer: {
|
||||
type: 'ref',
|
||||
ref: 'lex:app.bsky.feed.post#viewerState',
|
||||
},
|
||||
},
|
||||
},
|
||||
viewerState: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
repost: {
|
||||
type: 'string',
|
||||
},
|
||||
upvote: {
|
||||
type: 'string',
|
||||
},
|
||||
downvote: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
AppBskyFeedRepost: {
|
||||
@ -3086,6 +3010,7 @@ export const ids = {
|
||||
AppBskyActorUpdateProfile: 'app.bsky.actor.updateProfile',
|
||||
AppBskyEmbedExternal: 'app.bsky.embed.external',
|
||||
AppBskyEmbedImages: 'app.bsky.embed.images',
|
||||
AppBskyFeedFeedViewPost: 'app.bsky.feed.feedViewPost',
|
||||
AppBskyFeedGetAuthorFeed: 'app.bsky.feed.getAuthorFeed',
|
||||
AppBskyFeedGetPostThread: 'app.bsky.feed.getPostThread',
|
||||
AppBskyFeedGetRepostedBy: 'app.bsky.feed.getRepostedBy',
|
||||
|
30
packages/pds/src/lexicon/types/app/bsky/feed/feedViewPost.ts
Normal file
30
packages/pds/src/lexicon/types/app/bsky/feed/feedViewPost.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* GENERATED CODE - DO NOT MODIFY
|
||||
*/
|
||||
import * as AppBskyFeedPost from './post'
|
||||
import * as AppBskyActorRef from '../actor/ref'
|
||||
|
||||
export interface Main {
|
||||
post: AppBskyFeedPost.View
|
||||
reply?: ReplyRef
|
||||
reason?: ReasonTrend | ReasonRepost | { $type: string; [k: string]: unknown }
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface ReplyRef {
|
||||
root: AppBskyFeedPost.View
|
||||
parent: AppBskyFeedPost.View
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface ReasonTrend {
|
||||
by: AppBskyActorRef.WithInfo
|
||||
indexedAt: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface ReasonRepost {
|
||||
by: AppBskyActorRef.WithInfo
|
||||
indexedAt: string
|
||||
[k: string]: unknown
|
||||
}
|
@ -3,9 +3,7 @@
|
||||
*/
|
||||
import express from 'express'
|
||||
import { HandlerAuth } from '@atproto/xrpc-server'
|
||||
import * as AppBskyActorRef from '../actor/ref'
|
||||
import * as AppBskyEmbedImages from '../embed/images'
|
||||
import * as AppBskyEmbedExternal from '../embed/external'
|
||||
import * as AppBskyFeedFeedViewPost from './feedViewPost'
|
||||
|
||||
export interface QueryParams {
|
||||
author: string
|
||||
@ -17,7 +15,7 @@ export type InputSchema = undefined
|
||||
|
||||
export interface OutputSchema {
|
||||
cursor?: string
|
||||
feed: FeedItem[]
|
||||
feed: AppBskyFeedFeedViewPost.Main[]
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
@ -41,30 +39,3 @@ export type Handler<HA extends HandlerAuth = never> = (ctx: {
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
}) => Promise<HandlerOutput> | HandlerOutput
|
||||
|
||||
export interface FeedItem {
|
||||
uri: string
|
||||
cid: string
|
||||
author: AppBskyActorRef.WithInfo
|
||||
trendedBy?: AppBskyActorRef.WithInfo
|
||||
repostedBy?: AppBskyActorRef.WithInfo
|
||||
record: {}
|
||||
embed?:
|
||||
| AppBskyEmbedImages.Presented
|
||||
| AppBskyEmbedExternal.Presented
|
||||
| { $type: string; [k: string]: unknown }
|
||||
replyCount: number
|
||||
repostCount: number
|
||||
upvoteCount: number
|
||||
downvoteCount: number
|
||||
indexedAt: string
|
||||
myState?: MyState
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface MyState {
|
||||
repost?: string
|
||||
upvote?: string
|
||||
downvote?: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
@ -3,9 +3,7 @@
|
||||
*/
|
||||
import express from 'express'
|
||||
import { HandlerAuth } from '@atproto/xrpc-server'
|
||||
import * as AppBskyActorRef from '../actor/ref'
|
||||
import * as AppBskyEmbedImages from '../embed/images'
|
||||
import * as AppBskyEmbedExternal from '../embed/external'
|
||||
import * as AppBskyFeedPost from './post'
|
||||
|
||||
export interface QueryParams {
|
||||
uri: string
|
||||
@ -15,7 +13,10 @@ export interface QueryParams {
|
||||
export type InputSchema = undefined
|
||||
|
||||
export interface OutputSchema {
|
||||
thread: Post | NotFoundPost | { $type: string; [k: string]: unknown }
|
||||
thread:
|
||||
| ThreadViewPost
|
||||
| NotFoundPost
|
||||
| { $type: string; [k: string]: unknown }
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
@ -41,23 +42,17 @@ export type Handler<HA extends HandlerAuth = never> = (ctx: {
|
||||
res: express.Response
|
||||
}) => Promise<HandlerOutput> | HandlerOutput
|
||||
|
||||
export interface Post {
|
||||
uri: string
|
||||
cid: string
|
||||
author: AppBskyActorRef.WithInfo
|
||||
record: {}
|
||||
embed?:
|
||||
| AppBskyEmbedImages.Presented
|
||||
| AppBskyEmbedExternal.Presented
|
||||
export interface ThreadViewPost {
|
||||
post: AppBskyFeedPost.View
|
||||
parent?:
|
||||
| ThreadViewPost
|
||||
| NotFoundPost
|
||||
| { $type: string; [k: string]: unknown }
|
||||
parent?: Post | NotFoundPost | { $type: string; [k: string]: unknown }
|
||||
replyCount: number
|
||||
replies?: (Post | NotFoundPost | { $type: string; [k: string]: unknown })[]
|
||||
repostCount: number
|
||||
upvoteCount: number
|
||||
downvoteCount: number
|
||||
indexedAt: string
|
||||
myState?: MyState
|
||||
replies?: (
|
||||
| ThreadViewPost
|
||||
| NotFoundPost
|
||||
| { $type: string; [k: string]: unknown }
|
||||
)[]
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
@ -66,10 +61,3 @@ export interface NotFoundPost {
|
||||
notFound: true
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface MyState {
|
||||
repost?: string
|
||||
upvote?: string
|
||||
downvote?: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
@ -3,9 +3,7 @@
|
||||
*/
|
||||
import express from 'express'
|
||||
import { HandlerAuth } from '@atproto/xrpc-server'
|
||||
import * as AppBskyActorRef from '../actor/ref'
|
||||
import * as AppBskyEmbedImages from '../embed/images'
|
||||
import * as AppBskyEmbedExternal from '../embed/external'
|
||||
import * as AppBskyFeedFeedViewPost from './feedViewPost'
|
||||
|
||||
export interface QueryParams {
|
||||
algorithm?: string
|
||||
@ -17,7 +15,7 @@ export type InputSchema = undefined
|
||||
|
||||
export interface OutputSchema {
|
||||
cursor?: string
|
||||
feed: FeedItem[]
|
||||
feed: AppBskyFeedFeedViewPost.Main[]
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
@ -41,30 +39,3 @@ export type Handler<HA extends HandlerAuth = never> = (ctx: {
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
}) => Promise<HandlerOutput> | HandlerOutput
|
||||
|
||||
export interface FeedItem {
|
||||
uri: string
|
||||
cid: string
|
||||
author: AppBskyActorRef.WithInfo
|
||||
trendedBy?: AppBskyActorRef.WithInfo
|
||||
repostedBy?: AppBskyActorRef.WithInfo
|
||||
record: {}
|
||||
embed?:
|
||||
| AppBskyEmbedImages.Presented
|
||||
| AppBskyEmbedExternal.Presented
|
||||
| { $type: string; [k: string]: unknown }
|
||||
replyCount: number
|
||||
repostCount: number
|
||||
upvoteCount: number
|
||||
downvoteCount: number
|
||||
indexedAt: string
|
||||
myState?: MyState
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface MyState {
|
||||
repost?: string
|
||||
upvote?: string
|
||||
downvote?: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
import * as AppBskyEmbedImages from '../embed/images'
|
||||
import * as AppBskyEmbedExternal from '../embed/external'
|
||||
import * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef'
|
||||
import * as AppBskyActorRef from '../actor/ref'
|
||||
|
||||
export interface Record {
|
||||
text: string
|
||||
@ -36,3 +37,28 @@ export interface TextSlice {
|
||||
end: number
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface View {
|
||||
uri: string
|
||||
cid: string
|
||||
author: AppBskyActorRef.WithInfo
|
||||
record: {}
|
||||
embed?:
|
||||
| AppBskyEmbedImages.Presented
|
||||
| AppBskyEmbedExternal.Presented
|
||||
| { $type: string; [k: string]: unknown }
|
||||
replyCount: number
|
||||
repostCount: number
|
||||
upvoteCount: number
|
||||
downvoteCount: number
|
||||
indexedAt: string
|
||||
viewer: ViewerState
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface ViewerState {
|
||||
repost?: string
|
||||
upvote?: string
|
||||
downvote?: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
255
packages/pds/src/services/feed/index.ts
Normal file
255
packages/pds/src/services/feed/index.ts
Normal file
@ -0,0 +1,255 @@
|
||||
import { sql } from 'kysely'
|
||||
import * as common from '@atproto/common'
|
||||
import Database from '../../db'
|
||||
import { countAll } from '../../db/util'
|
||||
import { ImageUriBuilder } from '../../image/uri'
|
||||
import { Presented as PresentedImage } from '../../lexicon/types/app/bsky/embed/images'
|
||||
import { View as PostView } from '../../lexicon/types/app/bsky/feed/post'
|
||||
import { ActorViewMap, FeedEmbeds, PostInfoMap, FeedItemType } from './types'
|
||||
|
||||
export * from './types'
|
||||
|
||||
export class FeedService {
|
||||
constructor(public db: Database, public imgUriBuilder: ImageUriBuilder) {}
|
||||
|
||||
static creator(imgUriBuilder: ImageUriBuilder) {
|
||||
return (db: Database) => new FeedService(db, imgUriBuilder)
|
||||
}
|
||||
|
||||
selectPostQb() {
|
||||
return this.db.db
|
||||
.selectFrom('post')
|
||||
.select([
|
||||
sql<FeedItemType>`${'post'}`.as('type'),
|
||||
'uri as postUri',
|
||||
'cid as postCid',
|
||||
'creator as originatorDid',
|
||||
'creator as authorDid',
|
||||
'replyParent as replyParent',
|
||||
'replyRoot as replyRoot',
|
||||
'indexedAt as cursor',
|
||||
])
|
||||
}
|
||||
|
||||
selectRepostQb() {
|
||||
return this.db.db
|
||||
.selectFrom('repost')
|
||||
.innerJoin('post', 'post.uri', 'repost.subject')
|
||||
.select([
|
||||
sql<FeedItemType>`${'repost'}`.as('type'),
|
||||
'post.uri as postUri',
|
||||
'post.cid as postCid',
|
||||
'repost.creator as originatorDid',
|
||||
'post.creator as authorDid',
|
||||
'post.replyParent as replyParent',
|
||||
'post.replyRoot as replyRoot',
|
||||
'repost.indexedAt as cursor',
|
||||
])
|
||||
}
|
||||
|
||||
selectTrendQb() {
|
||||
return this.db.db
|
||||
.selectFrom('trend')
|
||||
.innerJoin('post', 'post.uri', 'trend.subject')
|
||||
.select([
|
||||
sql<FeedItemType>`${'trend'}`.as('type'),
|
||||
'post.uri as postUri',
|
||||
'post.cid as postCid',
|
||||
'trend.creator as originatorDid',
|
||||
'post.creator as authorDid',
|
||||
'post.replyParent as replyParent',
|
||||
'post.replyRoot as replyRoot',
|
||||
'trend.indexedAt as cursor',
|
||||
])
|
||||
}
|
||||
|
||||
async getActorViews(dids: string[]): Promise<ActorViewMap> {
|
||||
if (dids.length < 1) return {}
|
||||
const actors = await this.db.db
|
||||
.selectFrom('did_handle as actor')
|
||||
.where('actor.did', 'in', dids)
|
||||
.leftJoin('profile', 'profile.creator', 'actor.did')
|
||||
.select([
|
||||
'actor.did as did',
|
||||
'actor.declarationCid as declarationCid',
|
||||
'actor.actorType as actorType',
|
||||
'actor.handle as handle',
|
||||
'profile.displayName as displayName',
|
||||
'profile.avatarCid as avatarCid',
|
||||
])
|
||||
.execute()
|
||||
return actors.reduce((acc, cur) => {
|
||||
return {
|
||||
...acc,
|
||||
[cur.did]: {
|
||||
did: cur.did,
|
||||
declaration: {
|
||||
cid: cur.declarationCid,
|
||||
actorType: cur.actorType,
|
||||
},
|
||||
handle: cur.handle,
|
||||
displayName: cur.displayName ?? undefined,
|
||||
avatar: cur.avatarCid
|
||||
? this.imgUriBuilder.getCommonSignedUri('avatar', cur.avatarCid)
|
||||
: undefined,
|
||||
},
|
||||
}
|
||||
}, {} as ActorViewMap)
|
||||
}
|
||||
|
||||
async getPostViews(
|
||||
postUris: string[],
|
||||
requester: string,
|
||||
): Promise<PostInfoMap> {
|
||||
if (postUris.length < 1) return {}
|
||||
const db = this.db.db
|
||||
const { ref } = db.dynamic
|
||||
const posts = await db
|
||||
.selectFrom('post')
|
||||
.where('post.uri', 'in', postUris)
|
||||
.innerJoin('ipld_block', 'ipld_block.cid', 'post.cid')
|
||||
.select([
|
||||
'post.uri as uri',
|
||||
'post.cid as cid',
|
||||
'post.creator as creator',
|
||||
'ipld_block.content as recordBytes',
|
||||
'ipld_block.indexedAt as indexedAt',
|
||||
db
|
||||
.selectFrom('vote')
|
||||
.whereRef('subject', '=', ref('post.uri'))
|
||||
.where('direction', '=', 'up')
|
||||
.select(countAll.as('count'))
|
||||
.as('upvoteCount'),
|
||||
db
|
||||
.selectFrom('vote')
|
||||
.whereRef('subject', '=', ref('post.uri'))
|
||||
.where('direction', '=', 'down')
|
||||
.select(countAll.as('count'))
|
||||
.as('downvoteCount'),
|
||||
db
|
||||
.selectFrom('repost')
|
||||
.whereRef('subject', '=', ref('post.uri'))
|
||||
.select(countAll.as('count'))
|
||||
.as('repostCount'),
|
||||
db
|
||||
.selectFrom('post as reply')
|
||||
.whereRef('reply.replyParent', '=', ref('post.uri'))
|
||||
.select(countAll.as('count'))
|
||||
.as('replyCount'),
|
||||
db
|
||||
.selectFrom('repost')
|
||||
.where('creator', '=', requester)
|
||||
.whereRef('subject', '=', ref('post.uri'))
|
||||
.select('uri')
|
||||
.as('requesterRepost'),
|
||||
db
|
||||
.selectFrom('vote')
|
||||
.where('creator', '=', requester)
|
||||
.whereRef('subject', '=', ref('post.uri'))
|
||||
.where('direction', '=', 'up')
|
||||
.select('uri')
|
||||
.as('requesterUpvote'),
|
||||
db
|
||||
.selectFrom('vote')
|
||||
.where('creator', '=', requester)
|
||||
.whereRef('subject', '=', ref('post.uri'))
|
||||
.where('direction', '=', 'down')
|
||||
.select('uri')
|
||||
.as('requesterDownvote'),
|
||||
])
|
||||
.execute()
|
||||
return posts.reduce(
|
||||
(acc, cur) => ({
|
||||
...acc,
|
||||
[cur.uri]: cur,
|
||||
}),
|
||||
{} as PostInfoMap,
|
||||
)
|
||||
}
|
||||
|
||||
async embedsForPosts(uris: string[]): Promise<FeedEmbeds> {
|
||||
if (uris.length < 1) {
|
||||
return {}
|
||||
}
|
||||
const imgPromise = this.db.db
|
||||
.selectFrom('post_embed_image')
|
||||
.selectAll()
|
||||
.where('postUri', 'in', uris)
|
||||
.orderBy('postUri')
|
||||
.orderBy('position')
|
||||
.execute()
|
||||
const extPromise = this.db.db
|
||||
.selectFrom('post_embed_external')
|
||||
.selectAll()
|
||||
.where('postUri', 'in', uris)
|
||||
.execute()
|
||||
const [images, externals] = await Promise.all([imgPromise, extPromise])
|
||||
const imgEmbeds = images.reduce((acc, cur) => {
|
||||
if (!acc[cur.postUri]) {
|
||||
acc[cur.postUri] = {
|
||||
$type: 'app.bsky.embed.images#presented',
|
||||
images: [],
|
||||
}
|
||||
}
|
||||
acc[cur.postUri].images.push({
|
||||
thumb: this.imgUriBuilder.getCommonSignedUri(
|
||||
'feed_thumbnail',
|
||||
cur.imageCid,
|
||||
),
|
||||
fullsize: this.imgUriBuilder.getCommonSignedUri(
|
||||
'feed_fullsize',
|
||||
cur.imageCid,
|
||||
),
|
||||
alt: cur.alt,
|
||||
})
|
||||
return acc
|
||||
}, {} as { [uri: string]: PresentedImage })
|
||||
return externals.reduce((acc, cur) => {
|
||||
if (!acc[cur.postUri]) {
|
||||
acc[cur.postUri] = {
|
||||
$type: 'app.bsky.embed.external#presented',
|
||||
external: {
|
||||
uri: cur.uri,
|
||||
title: cur.title,
|
||||
description: cur.description,
|
||||
thumb: cur.thumbCid
|
||||
? this.imgUriBuilder.getCommonSignedUri(
|
||||
'feed_thumbnail',
|
||||
cur.thumbCid,
|
||||
)
|
||||
: undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
return acc
|
||||
}, imgEmbeds as FeedEmbeds)
|
||||
}
|
||||
|
||||
formatPostView(
|
||||
uri: string,
|
||||
actors: ActorViewMap,
|
||||
posts: PostInfoMap,
|
||||
embeds: FeedEmbeds,
|
||||
): PostView | undefined {
|
||||
const post = posts[uri]
|
||||
const author = actors[post.creator]
|
||||
if (!post || !author) return undefined
|
||||
return {
|
||||
uri: post.uri,
|
||||
cid: post.cid,
|
||||
author: author,
|
||||
record: common.ipldBytesToRecord(post.recordBytes),
|
||||
embed: embeds[uri],
|
||||
replyCount: post.replyCount,
|
||||
repostCount: post.repostCount,
|
||||
upvoteCount: post.upvoteCount,
|
||||
downvoteCount: post.downvoteCount,
|
||||
indexedAt: post.indexedAt,
|
||||
viewer: {
|
||||
repost: post.requesterRepost ?? undefined,
|
||||
upvote: post.requesterUpvote ?? undefined,
|
||||
downvote: post.requesterDownvote ?? undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
48
packages/pds/src/services/feed/types.ts
Normal file
48
packages/pds/src/services/feed/types.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { Presented as PresentedImage } from '../../lexicon/types/app/bsky/embed/images'
|
||||
import { Presented as PresentedExternal } from '../../lexicon/types/app/bsky/embed/external'
|
||||
|
||||
export type FeedEmbeds = {
|
||||
[uri: string]: PresentedImage | PresentedExternal
|
||||
}
|
||||
|
||||
export type PostInfo = {
|
||||
uri: string
|
||||
cid: string
|
||||
creator: string
|
||||
recordBytes: Uint8Array
|
||||
indexedAt: string
|
||||
upvoteCount: number
|
||||
downvoteCount: number
|
||||
repostCount: number
|
||||
replyCount: number
|
||||
requesterRepost: string | null
|
||||
requesterUpvote: string | null
|
||||
requesterDownvote: string | null
|
||||
}
|
||||
|
||||
export type PostInfoMap = { [uri: string]: PostInfo }
|
||||
|
||||
export type ActorView = {
|
||||
did: string
|
||||
declaration: {
|
||||
cid: string
|
||||
actorType: string
|
||||
}
|
||||
handle: string
|
||||
displayName?: string
|
||||
avatar?: string
|
||||
}
|
||||
export type ActorViewMap = { [did: string]: ActorView }
|
||||
|
||||
export type FeedItemType = 'post' | 'repost' | 'trend'
|
||||
|
||||
export type FeedRow = {
|
||||
type: FeedItemType
|
||||
postUri: string
|
||||
postCid: string
|
||||
originatorDid: string
|
||||
authorDid: string
|
||||
replyParent: string | null
|
||||
replyRoot: string | null
|
||||
cursor: string
|
||||
}
|
@ -1,18 +1,23 @@
|
||||
import { BlobStore } from '@atproto/repo'
|
||||
import Database from '../db'
|
||||
import { MessageQueue } from '../event-stream/types'
|
||||
import { ImageUriBuilder } from '../image/uri'
|
||||
import { ActorService } from './actor'
|
||||
import { AuthService } from './auth'
|
||||
import { FeedService } from './feed'
|
||||
import { RecordService } from './record'
|
||||
import { RepoService } from './repo'
|
||||
|
||||
export function createServices(
|
||||
messageQueue: MessageQueue,
|
||||
blobstore: BlobStore,
|
||||
): Services {
|
||||
export function createServices(resources: {
|
||||
messageQueue: MessageQueue
|
||||
blobstore: BlobStore
|
||||
imgUriBuilder: ImageUriBuilder
|
||||
}): Services {
|
||||
const { messageQueue, blobstore, imgUriBuilder } = resources
|
||||
return {
|
||||
actor: ActorService.creator(),
|
||||
auth: AuthService.creator(),
|
||||
feed: FeedService.creator(imgUriBuilder),
|
||||
record: RecordService.creator(messageQueue),
|
||||
repo: RepoService.creator(messageQueue, blobstore),
|
||||
}
|
||||
@ -21,6 +26,7 @@ export function createServices(
|
||||
export type Services = {
|
||||
actor: FromDb<ActorService>
|
||||
auth: FromDb<AuthService>
|
||||
feed: FromDb<FeedService>
|
||||
record: FromDb<RecordService>
|
||||
repo: FromDb<RepoService>
|
||||
}
|
||||
|
@ -8,8 +8,7 @@ import { randomStr } from '@atproto/crypto'
|
||||
import { CID } from 'multiformats/cid'
|
||||
import * as uint8arrays from 'uint8arrays'
|
||||
import { PDS, ServerConfig, Database, MemoryBlobStore } from '../src/index'
|
||||
import * as GetAuthorFeed from '../src/lexicon/types/app/bsky/feed/getAuthorFeed'
|
||||
import * as GetTimeline from '../src/lexicon/types/app/bsky/feed/getTimeline'
|
||||
import { Main as FeedViewPost } from '../src/lexicon/types/app/bsky/feed/feedViewPost'
|
||||
import DiskBlobStore from '../src/storage/disk-blobstore'
|
||||
import AppContext from '../src/context'
|
||||
|
||||
@ -158,10 +157,13 @@ export const forSnapshot = (obj: unknown) => {
|
||||
|
||||
// Feed testing utils
|
||||
|
||||
type FeedItem = GetAuthorFeed.FeedItem & GetTimeline.FeedItem
|
||||
|
||||
export const getOriginator = (item: FeedItem) =>
|
||||
item.repostedBy ? item.repostedBy.did : item.author.did
|
||||
export const getOriginator = (item: FeedViewPost) => {
|
||||
if (!item.reason) {
|
||||
return item.post.author.did
|
||||
} else {
|
||||
return (item.reason.by as { [did: string]: string }).did
|
||||
}
|
||||
}
|
||||
|
||||
// Useful for remapping ids in snapshot testing, to make snapshots deterministic.
|
||||
// E.g. you may use this to map this:
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -78,10 +78,11 @@ describe('pds author feed views', () => {
|
||||
},
|
||||
)
|
||||
|
||||
aliceForCarol.data.feed.forEach(({ uri, myState }) => {
|
||||
expect(myState?.upvote).toEqual(sc.votes.up[carol]?.[uri]?.toString())
|
||||
expect(myState?.downvote).toEqual(sc.votes.down[carol]?.[uri]?.toString())
|
||||
expect(myState?.repost).toEqual(sc.reposts[carol][uri]?.toString())
|
||||
aliceForCarol.data.feed.forEach((postView) => {
|
||||
const { viewer, uri } = postView.post
|
||||
expect(viewer?.upvote).toEqual(sc.votes.up[carol]?.[uri]?.toString())
|
||||
expect(viewer?.downvote).toEqual(sc.votes.down[carol]?.[uri]?.toString())
|
||||
expect(viewer?.repost).toEqual(sc.reposts[carol][uri]?.toString())
|
||||
})
|
||||
|
||||
expect(forSnapshot(aliceForCarol.data.feed)).toMatchSnapshot()
|
||||
|
@ -1,5 +1,5 @@
|
||||
import AtpApi, { ServiceClient as AtpServiceClient } from '@atproto/api'
|
||||
import * as Timeline from '../../src/lexicon/types/app/bsky/feed/getTimeline'
|
||||
import { Main as FeedViewPost } from '../../src/lexicon/types/app/bsky/feed/feedViewPost'
|
||||
import {
|
||||
runTestServer,
|
||||
forSnapshot,
|
||||
@ -41,7 +41,7 @@ describe('timeline views', () => {
|
||||
})
|
||||
|
||||
it("fetches authenticated user's home feed w/ reverse-chronological algorithm", async () => {
|
||||
const expectOriginatorFollowedBy = (did) => (item: Timeline.FeedItem) => {
|
||||
const expectOriginatorFollowedBy = (did) => (item: FeedViewPost) => {
|
||||
const originator = getOriginator(item)
|
||||
// The user expects to see posts & reposts from themselves and follows
|
||||
if (did !== originator) {
|
||||
@ -90,26 +90,6 @@ describe('timeline views', () => {
|
||||
danTL.data.feed.forEach(expectOriginatorFollowedBy(dan))
|
||||
})
|
||||
|
||||
it("fetches authenticated user's home feed w/ firehose algorithm", async () => {
|
||||
const aliceTL = await client.app.bsky.feed.getTimeline(
|
||||
{ algorithm: FeedAlgorithm.Firehose },
|
||||
{
|
||||
headers: sc.getHeaders(alice),
|
||||
},
|
||||
)
|
||||
|
||||
expect(forSnapshot(aliceTL.data.feed)).toMatchSnapshot()
|
||||
|
||||
const carolTL = await client.app.bsky.feed.getTimeline(
|
||||
{ algorithm: FeedAlgorithm.Firehose },
|
||||
{
|
||||
headers: sc.getHeaders(carol),
|
||||
},
|
||||
)
|
||||
|
||||
expect(forSnapshot(carolTL.data.feed)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("fetches authenticated user's home feed w/ default algorithm", async () => {
|
||||
const defaultTL = await client.app.bsky.feed.getTimeline(
|
||||
{},
|
||||
@ -155,34 +135,4 @@ describe('timeline views', () => {
|
||||
expect(full.data.feed.length).toEqual(7)
|
||||
expect(results(paginatedAll)).toEqual(results([full.data]))
|
||||
})
|
||||
|
||||
it('paginates firehose feed', async () => {
|
||||
const results = (results) => results.flatMap((res) => res.feed)
|
||||
const paginator = async (cursor?: string) => {
|
||||
const res = await client.app.bsky.feed.getTimeline(
|
||||
{
|
||||
algorithm: FeedAlgorithm.Firehose,
|
||||
before: cursor,
|
||||
limit: 5,
|
||||
},
|
||||
{ headers: sc.getHeaders(alice) },
|
||||
)
|
||||
return res.data
|
||||
}
|
||||
|
||||
const paginatedAll = await paginateAll(paginator)
|
||||
paginatedAll.forEach((res) =>
|
||||
expect(res.feed.length).toBeLessThanOrEqual(5),
|
||||
)
|
||||
|
||||
const full = await client.app.bsky.feed.getTimeline(
|
||||
{
|
||||
algorithm: FeedAlgorithm.Firehose,
|
||||
},
|
||||
{ headers: sc.getHeaders(alice) },
|
||||
)
|
||||
|
||||
expect(full.data.feed.length).toEqual(15)
|
||||
expect(results(paginatedAll)).toEqual(results([full.data]))
|
||||
})
|
||||
})
|
||||
|
@ -161,10 +161,14 @@ describe('pds vote views', () => {
|
||||
}
|
||||
|
||||
post = await getPost()
|
||||
console.log(post.thread)
|
||||
expect(
|
||||
(post.thread as AppBskyFeedGetPostThread.Post).downvoteCount,
|
||||
(post.thread.post as AppBskyFeedGetPostThread.ThreadViewPost)
|
||||
.downvoteCount,
|
||||
).toEqual(0)
|
||||
expect((post.thread as AppBskyFeedGetPostThread.Post).myState).toEqual({})
|
||||
expect(
|
||||
(post.thread.post as AppBskyFeedGetPostThread.ThreadViewPost).viewer,
|
||||
).toEqual({})
|
||||
|
||||
// Upvote
|
||||
const { data: upvoted } = await client.app.bsky.feed.setVote(
|
||||
@ -181,14 +185,16 @@ describe('pds vote views', () => {
|
||||
expect(upvoted.upvote).not.toBeUndefined()
|
||||
expect(upvoted.downvote).toBeUndefined()
|
||||
expect(
|
||||
(post.thread as AppBskyFeedGetPostThread.Post).upvoteCount,
|
||||
(post.thread.post as AppBskyFeedGetPostThread.ThreadViewPost)
|
||||
.upvoteCount,
|
||||
).toEqual(1)
|
||||
expect(
|
||||
(post.thread as AppBskyFeedGetPostThread.Post).downvoteCount,
|
||||
(post.thread.post as AppBskyFeedGetPostThread.ThreadViewPost)
|
||||
.downvoteCount,
|
||||
).toEqual(0)
|
||||
expect((post.thread as AppBskyFeedGetPostThread.Post).myState).toEqual(
|
||||
upvoted,
|
||||
)
|
||||
expect(
|
||||
(post.thread.post as AppBskyFeedGetPostThread.ThreadViewPost).viewer,
|
||||
).toEqual(upvoted)
|
||||
|
||||
// Downvote
|
||||
const { data: downvoted } = await client.app.bsky.feed.setVote(
|
||||
@ -205,14 +211,16 @@ describe('pds vote views', () => {
|
||||
expect(downvoted.upvote).toBeUndefined()
|
||||
expect(downvoted.downvote).not.toBeUndefined()
|
||||
expect(
|
||||
(post.thread as AppBskyFeedGetPostThread.Post).upvoteCount,
|
||||
(post.thread.post as AppBskyFeedGetPostThread.ThreadViewPost)
|
||||
.upvoteCount,
|
||||
).toEqual(0)
|
||||
expect(
|
||||
(post.thread as AppBskyFeedGetPostThread.Post).downvoteCount,
|
||||
(post.thread.post as AppBskyFeedGetPostThread.ThreadViewPost)
|
||||
.downvoteCount,
|
||||
).toEqual(1)
|
||||
expect((post.thread as AppBskyFeedGetPostThread.Post).myState).toEqual(
|
||||
downvoted,
|
||||
)
|
||||
expect(
|
||||
(post.thread.post as AppBskyFeedGetPostThread.ThreadViewPost).viewer,
|
||||
).toEqual(downvoted)
|
||||
|
||||
// No vote
|
||||
const { data: novoted } = await client.app.bsky.feed.setVote(
|
||||
@ -229,14 +237,16 @@ describe('pds vote views', () => {
|
||||
expect(novoted.upvote).toBeUndefined()
|
||||
expect(novoted.downvote).toBeUndefined()
|
||||
expect(
|
||||
(post.thread as AppBskyFeedGetPostThread.Post).upvoteCount,
|
||||
(post.thread.post as AppBskyFeedGetPostThread.ThreadViewPost)
|
||||
.upvoteCount,
|
||||
).toEqual(0)
|
||||
expect(
|
||||
(post.thread as AppBskyFeedGetPostThread.Post).downvoteCount,
|
||||
(post.thread.post as AppBskyFeedGetPostThread.ThreadViewPost)
|
||||
.downvoteCount,
|
||||
).toEqual(0)
|
||||
expect((post.thread as AppBskyFeedGetPostThread.Post).myState).toEqual(
|
||||
novoted,
|
||||
)
|
||||
expect(
|
||||
(post.thread.post as AppBskyFeedGetPostThread.ThreadViewPost).viewer,
|
||||
).toEqual(novoted)
|
||||
})
|
||||
|
||||
it('no-ops when already in correct state', async () => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user