lex refactor hot fixes ()

* filter embeds from get popular

* Truncate profile info to satisfy validation ()

* Remove trailing replacement character when utf8-truncating ()

* Truncate profile info to satisfy validation

* Fix utf8 truncation w/ replacement character

* filter replies

* delete embeds from records on getPopular

* Update profile display name and description lengths to be based on graphemes ()

* @atproto api v0.2.1

* Tidy

---------

Co-authored-by: devin ivy <devinivy@gmail.com>
This commit is contained in:
Daniel Holmgren 2023-04-04 14:56:53 -05:00 committed by GitHub
parent 1014938520
commit eb488b96f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 114 additions and 32 deletions
lexicons/app/bsky/actor
packages
api/src/client
pds
src
app-view
api/app/bsky
services
lexicon
tests/views
yarn.lock

@ -11,7 +11,8 @@
"handle": {"type": "string", "format": "handle"},
"displayName": {
"type": "string",
"maxLength": 64
"maxGraphemes": 64,
"maxLength": 640
},
"avatar": { "type": "string" },
"viewer": {"type": "ref", "ref": "#viewerState"}
@ -25,11 +26,13 @@
"handle": {"type": "string", "format":"handle"},
"displayName": {
"type": "string",
"maxLength": 64
"maxGraphemes": 64,
"maxLength": 640
},
"description": {
"type": "string",
"maxLength": 256
"maxGraphemes": 256,
"maxLength": 2560
},
"avatar": { "type": "string" },
"indexedAt": {"type": "string", "format": "datetime"},
@ -44,11 +47,13 @@
"handle": {"type": "string", "format": "handle"},
"displayName": {
"type": "string",
"maxLength": 64
"maxGraphemes": 64,
"maxLength": 640
},
"description": {
"type": "string",
"maxLength": 256
"maxGraphemes": 256,
"maxLength": 2560
},
"avatar": { "type": "string" },
"banner": { "type": "string" },

@ -10,11 +10,13 @@
"properties": {
"displayName": {
"type": "string",
"maxLength": 64
"maxGraphemes": 64,
"maxLength": 640
},
"description": {
"type": "string",
"maxLength": 256
"maxGraphemes": 256,
"maxLength": 2560
},
"avatar": {
"type": "blob",

@ -2533,7 +2533,8 @@ export const schemaDict = {
},
displayName: {
type: 'string',
maxLength: 64,
maxGraphemes: 64,
maxLength: 640,
},
avatar: {
type: 'string',
@ -2558,11 +2559,13 @@ export const schemaDict = {
},
displayName: {
type: 'string',
maxLength: 64,
maxGraphemes: 64,
maxLength: 640,
},
description: {
type: 'string',
maxLength: 256,
maxGraphemes: 256,
maxLength: 2560,
},
avatar: {
type: 'string',
@ -2591,11 +2594,13 @@ export const schemaDict = {
},
displayName: {
type: 'string',
maxLength: 64,
maxGraphemes: 64,
maxLength: 640,
},
description: {
type: 'string',
maxLength: 256,
maxGraphemes: 256,
maxLength: 2560,
},
avatar: {
type: 'string',
@ -2761,11 +2766,13 @@ export const schemaDict = {
properties: {
displayName: {
type: 'string',
maxLength: 64,
maxGraphemes: 64,
maxLength: 640,
},
description: {
type: 'string',
maxLength: 256,
maxGraphemes: 256,
maxLength: 2560,
},
avatar: {
type: 'blob',

@ -4,6 +4,7 @@ import { paginate } from '../../../../db/pagination'
import AppContext from '../../../../context'
import { FeedRow, FeedItemType } from '../../../services/feed'
import { sql } from 'kysely'
import { FeedViewPost } from '../../../../lexicon/types/app/bsky/feed/defs'
// THIS IS A TEMPORARY UNSPECCED ROUTE
export default function (server: Server, ctx: AppContext) {
@ -51,12 +52,25 @@ export default function (server: Server, ctx: AppContext) {
feedQb = paginate(feedQb, { limit, cursor, keyset })
const feedItems: FeedRow[] = await feedQb.execute()
const feed = await composeFeed(feedService, feedItems, requester)
const feed: FeedViewPost[] = await composeFeed(
feedService,
feedItems,
requester,
)
const noRecordEmbeds = feed.map((post) => {
delete post.post.record['embed']
if (post.reply) {
delete post.reply.parent.record['embed']
delete post.reply.root.record['embed']
}
return post
})
return {
encoding: 'application/json',
body: {
feed,
feed: noRecordEmbeds,
cursor: keyset.packFromResult(feedItems),
},
}

@ -96,8 +96,8 @@ export class ActorViews {
return {
did: result.did,
handle: result.handle,
displayName: profileInfo?.displayName || undefined,
description: profileInfo?.description || undefined,
displayName: truncateUtf8(profileInfo?.displayName, 64) || undefined,
description: truncateUtf8(profileInfo?.description, 256) || undefined,
avatar,
banner,
followsCount: profileInfo?.followsCount ?? 0,
@ -174,8 +174,8 @@ export class ActorViews {
return {
did: result.did,
handle: result.handle,
displayName: profileInfo?.displayName || undefined,
description: profileInfo?.description || undefined,
displayName: truncateUtf8(profileInfo?.displayName, 64) || undefined,
description: truncateUtf8(profileInfo?.description, 256) || undefined,
avatar,
indexedAt: profileInfo?.indexedAt || undefined,
viewer: {
@ -206,7 +206,7 @@ export class ActorViews {
const views = profiles.map((view) => ({
did: view.did,
handle: view.handle,
displayName: view.displayName,
displayName: truncateUtf8(view.displayName, 64) || undefined,
avatar: view.avatar,
viewer: view.viewer,
}))
@ -216,3 +216,15 @@ export class ActorViews {
}
type ActorResult = DidHandle
function truncateUtf8(str: string | null | undefined, length: number) {
if (!str) return str
const encoder = new TextEncoder()
const utf8 = encoder.encode(str)
if (utf8.length > length) {
const decoder = new TextDecoder('utf-8', { fatal: false })
const truncated = utf8.slice(0, length)
return decoder.decode(truncated).replace(/\uFFFD$/, '')
}
return str
}

@ -109,7 +109,7 @@ export class FeedService {
[cur.did]: {
did: cur.did,
handle: cur.handle,
displayName: cur.displayName || undefined,
displayName: truncateUtf8(cur.displayName, 64) || undefined,
avatar: cur.avatarCid
? this.imgUriBuilder.getCommonSignedUri('avatar', cur.avatarCid)
: undefined,
@ -350,3 +350,15 @@ export class FeedService {
}
}
}
function truncateUtf8(str: string | null | undefined, length: number) {
if (!str) return str
const encoder = new TextEncoder()
const utf8 = encoder.encode(str)
if (utf8.length > length) {
const decoder = new TextDecoder('utf-8', { fatal: false })
const truncated = utf8.slice(0, length)
return decoder.decode(truncated).replace(/\uFFFD$/, '')
}
return str
}

@ -2533,7 +2533,8 @@ export const schemaDict = {
},
displayName: {
type: 'string',
maxLength: 64,
maxGraphemes: 64,
maxLength: 640,
},
avatar: {
type: 'string',
@ -2558,11 +2559,13 @@ export const schemaDict = {
},
displayName: {
type: 'string',
maxLength: 64,
maxGraphemes: 64,
maxLength: 640,
},
description: {
type: 'string',
maxLength: 256,
maxGraphemes: 256,
maxLength: 2560,
},
avatar: {
type: 'string',
@ -2591,11 +2594,13 @@ export const schemaDict = {
},
displayName: {
type: 'string',
maxLength: 64,
maxGraphemes: 64,
maxLength: 640,
},
description: {
type: 'string',
maxLength: 256,
maxGraphemes: 256,
maxLength: 2560,
},
avatar: {
type: 'string',
@ -2761,11 +2766,13 @@ export const schemaDict = {
properties: {
displayName: {
type: 'string',
maxLength: 64,
maxGraphemes: 64,
maxLength: 640,
},
description: {
type: 'string',
maxLength: 256,
maxGraphemes: 256,
maxLength: 2560,
},
avatar: {
type: 'blob',

@ -54,24 +54,36 @@ describe('popular views', () => {
})
it('returns well liked posts', async () => {
const one = await sc.post(alice, 'like this')
const img = await sc.uploadFile(
alice,
'tests/image/fixtures/key-landscape-small.jpg',
'image/jpeg',
)
const one = await sc.post(alice, 'first post', undefined, [img])
await sc.like(bob, one.ref)
await sc.like(carol, one.ref)
await sc.like(dan, one.ref)
await sc.like(eve, one.ref)
await sc.like(frank, one.ref)
const two = await sc.post(bob, 'like this')
const two = await sc.post(bob, 'bobby boi')
await sc.like(alice, two.ref)
await sc.like(carol, two.ref)
await sc.like(dan, two.ref)
await sc.like(eve, two.ref)
await sc.like(frank, two.ref)
const three = await sc.reply(bob, one.ref, one.ref, 'reply')
await sc.like(alice, three.ref)
await sc.like(carol, three.ref)
await sc.like(dan, three.ref)
await sc.like(eve, three.ref)
await sc.like(frank, three.ref)
const res = await agent.api.app.bsky.unspecced.getPopular(
{},
{ headers: sc.getHeaders(alice) },
)
const feedUris = res.data.feed.map((i) => i.post.uri).sort()
const expected = [one.ref.uriStr, two.ref.uriStr].sort()
const expected = [one.ref.uriStr, two.ref.uriStr, three.ref.uriStr].sort()
expect(feedUris).toEqual(expected)
})
})

@ -20,6 +20,17 @@
pino "^8.6.1"
zod "^3.14.2"
"@atproto/crypto@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@atproto/crypto/-/crypto-0.1.0.tgz#bc73a479f9dbe06fa025301c182d7f7ab01bc568"
integrity sha512-9xgFEPtsCiJEPt9o3HtJT30IdFTGw5cQRSJVIy5CFhqBA4vDLcdXiRDLCjkzHEVbtNCsHUW6CrlfOgbeLPcmcg==
dependencies:
"@noble/secp256k1" "^1.7.0"
big-integer "^1.6.51"
multiformats "^9.6.4"
one-webcrypto "^1.0.3"
uint8arrays "3.0.0"
"@aws-crypto/crc32@2.0.0":
version "2.0.0"
resolved "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-2.0.0.tgz"