Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable creation of unknown record types #2171

Merged
merged 5 commits into from Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 0 additions & 5 deletions packages/pds/src/api/com/atproto/repo/applyWrites.ts
Expand Up @@ -56,11 +56,6 @@ export default function (server: Server, ctx: AppContext) {
if (did !== auth.credentials.did) {
throw new AuthRequiredError()
}
if (validate === false) {
throw new InvalidRequestError(
'Unvalidated writes are not yet supported.',
)
}
if (tx.writes.length > 200) {
throw new InvalidRequestError('Too many writes. Max: 200')
}
Expand Down
5 changes: 0 additions & 5 deletions packages/pds/src/api/com/atproto/repo/createRecord.ts
Expand Up @@ -36,11 +36,6 @@ export default function (server: Server, ctx: AppContext) {
if (did !== auth.credentials.did) {
throw new AuthRequiredError()
}
if (validate === false) {
throw new InvalidRequestError(
'Unvalidated writes are not yet supported.',
)
}
const swapCommitCid = swapCommit ? CID.parse(swapCommit) : undefined

let write: PreparedCreate
Expand Down
5 changes: 0 additions & 5 deletions packages/pds/src/api/com/atproto/repo/putRecord.ts
Expand Up @@ -46,11 +46,6 @@ export default function (server: Server, ctx: AppContext) {
if (did !== auth.credentials.did) {
throw new AuthRequiredError()
}
if (validate === false) {
throw new InvalidRequestError(
'Unvalidated writes are not yet supported.',
)
}

const uri = AtUri.make(did, collection, rkey)
const swapCommitCid = swapCommit ? CID.parse(swapCommit) : undefined
Expand Down
219 changes: 95 additions & 124 deletions packages/pds/src/repo/prepare.ts
Expand Up @@ -4,12 +4,15 @@ import {
ensureValidRecordKey,
ensureValidDatetime,
} from '@atproto/syntax'
import { MINUTE, TID, dataToCborBlock } from '@atproto/common'
import { TID, check, dataToCborBlock } from '@atproto/common'
import {
BlobRef,
LexValue,
LexiconDefNotFoundError,
RepoRecord,
ValidationError,
lexToIpld,
untypedJsonBlobRef,
} from '@atproto/lexicon'
import {
cborToLex,
Expand All @@ -28,91 +31,12 @@ import {
PreparedBlobRef,
} from './types'
import * as lex from '../lexicon/lexicons'
import { isMain as isExternalEmbed } from '../lexicon/types/app/bsky/embed/external'
import { isMain as isImagesEmbed } from '../lexicon/types/app/bsky/embed/images'
import { isMain as isRecordWithMediaEmbed } from '../lexicon/types/app/bsky/embed/recordWithMedia'
import { isRecord as isFeedGenerator } from '../lexicon/types/app/bsky/feed/generator'
import {
Record as PostRecord,
isRecord as isPost,
} from '../lexicon/types/app/bsky/feed/post'
import { isRecord as isPost } from '../lexicon/types/app/bsky/feed/post'
import { isTag } from '../lexicon/types/app/bsky/richtext/facet'
import { isRecord as isList } from '../lexicon/types/app/bsky/graph/list'
import { isRecord as isProfile } from '../lexicon/types/app/bsky/actor/profile'
import { hasExplicitSlur } from '../handle/explicit-slurs'
import { InvalidRequestError } from '@atproto/xrpc-server'

// @TODO do this dynamically off of schemas
export const blobsForWrite = (record: unknown): PreparedBlobRef[] => {
if (isProfile(record)) {
const doc = lex.schemaDict.AppBskyActorProfile
const refs: PreparedBlobRef[] = []
if (record.avatar) {
refs.push({
cid: record.avatar.ref,
mimeType: record.avatar.mimeType,
constraints: doc.defs.main.record.properties.avatar,
})
}
if (record.banner) {
refs.push({
cid: record.banner.ref,
mimeType: record.banner.mimeType,
constraints: doc.defs.main.record.properties.banner,
})
}
return refs
} else if (isFeedGenerator(record)) {
const doc = lex.schemaDict.AppBskyFeedGenerator
if (!record.avatar) {
return []
}
return [
{
cid: record.avatar.ref,
mimeType: record.avatar.mimeType,
constraints: doc.defs.main.record.properties.avatar,
},
]
} else if (isList(record)) {
const doc = lex.schemaDict.AppBskyGraphList
if (!record.avatar) {
return []
}
return [
{
cid: record.avatar.ref,
mimeType: record.avatar.mimeType,
constraints: doc.defs.main.record.properties.avatar,
},
]
} else if (isPost(record)) {
const refs: PreparedBlobRef[] = []
const embeds = separateEmbeds(record.embed)
for (const embed of embeds) {
if (isImagesEmbed(embed)) {
const doc = lex.schemaDict.AppBskyEmbedImages
for (let i = 0; i < embed.images.length || 0; i++) {
const img = embed.images[i]
refs.push({
cid: img.image.ref,
mimeType: img.image.mimeType,
constraints: doc.defs.image.properties.image,
})
}
} else if (isExternalEmbed(embed) && embed.external.thumb) {
const doc = lex.schemaDict.AppBskyEmbedExternal
refs.push({
cid: embed.external.thumb.ref,
mimeType: embed.external.thumb.mimeType,
constraints: doc.defs.external.properties.thumb,
})
}
}
return refs
}
return []
}

export const assertValidRecord = (record: Record<string, unknown>) => {
if (typeof record.$type !== 'string') {
Expand Down Expand Up @@ -180,17 +104,6 @@ export const prepareCreate = async (opts: {
}

const nextRkey = TID.next()
if (
collection === lex.ids.AppBskyFeedPost &&
opts.rkey &&
!rkeyIsInWindow(nextRkey, new TID(opts.rkey))
) {
// @TODO temporary. allowing a window supports creation of post and gate records at the same time.
throw new InvalidRequestError(
'Custom rkeys for post records should be near the present.',
)
}

const rkey = opts.rkey || nextRkey.toString()
// @TODO: validate against Lexicon record 'key' type, not just overall recordkey syntax
ensureValidRecordKey(rkey)
Expand All @@ -201,17 +114,10 @@ export const prepareCreate = async (opts: {
cid: await cidForSafeRecord(record),
swapCid,
record,
blobs: blobsForWrite(record),
blobs: blobsForWrite(record, validate),
}
}

// only allow PUTs to certain collections
const ALLOWED_PUTS = [
lex.ids.AppBskyActorProfile,
lex.ids.AppBskyGraphList,
lex.ids.AppBskyFeedGenerator,
]

export const prepareUpdate = async (opts: {
did: string
collection: string
Expand All @@ -221,15 +127,6 @@ export const prepareUpdate = async (opts: {
validate?: boolean
}): Promise<PreparedUpdate> => {
const { did, collection, rkey, swapCid, validate = true } = opts
if (!ALLOWED_PUTS.includes(collection)) {
// @TODO temporary
throw new InvalidRequestError(
`Temporarily only accepting updates for collections: ${ALLOWED_PUTS.join(
', ',
)}`,
)
}

const record = setCollectionName(collection, opts.record, validate)
if (validate) {
assertValidRecord(record)
Expand All @@ -241,7 +138,7 @@ export const prepareUpdate = async (opts: {
cid: await cidForSafeRecord(record),
swapCid,
record,
blobs: blobsForWrite(record),
blobs: blobsForWrite(record, validate),
}
}

Expand Down Expand Up @@ -292,16 +189,6 @@ export const writeToOp = (write: PreparedWrite): RecordWriteOp => {
}
}

function separateEmbeds(embed: PostRecord['embed']) {
if (!embed) {
return []
}
if (isRecordWithMediaEmbed(embed)) {
return [{ $type: lex.ids.AppBskyEmbedRecord, ...embed.record }, embed.media]
}
return [embed]
}

async function cidForSafeRecord(record: RepoRecord) {
try {
const block = await dataToCborBlock(lexToIpld(record))
Expand Down Expand Up @@ -342,8 +229,92 @@ function assertNoExplicitSlurs(rkey: string, record: RepoRecord) {
}
}

// ensures two rkeys are not far apart
function rkeyIsInWindow(rkey1: TID, rkey2: TID) {
const ms = Math.abs(rkey1.timestamp() - rkey2.timestamp()) / 1000
return ms < 10 * MINUTE
type FoundBlobRef = {
ref: BlobRef
path: string[]
}

export const blobsForWrite = (
record: RepoRecord,
validate: boolean,
): PreparedBlobRef[] => {
const refs = findBlobRefs(record)
const recordType =
typeof record['$type'] === 'string' ? record['$type'] : undefined

for (const ref of refs) {
if (check.is(ref.ref.original, untypedJsonBlobRef)) {
throw new InvalidRecordError(`Legacy blob ref at '${ref.path.join('/')}'`)
}
}

return refs.map(({ ref, path }) => ({
cid: ref.ref,
mimeType: ref.mimeType,
constraints:
validate && recordType
? CONSTRAINTS[recordType]?.[path.join('/')] ?? {}
: {},
}))
}

export const findBlobRefs = (
val: LexValue,
path: string[] = [],
layer = 0,
): FoundBlobRef[] => {
if (layer > 10) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a gut check that 10 layers is enough for e.g. the images within a record-with-media (counting in my head I suppose it's probably fine, close to 5?). It just feels a little conservative, I'd be down to increase this a bit either way.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup fair point, I can bump it up 👍

return []
}
// walk arrays
if (Array.isArray(val)) {
return val.flatMap((item) => findBlobRefs(item, path, layer + 1))
}
// objects
if (val && typeof val === 'object') {
// convert blobs, leaving the original encoding so that we don't change CIDs on re-encode
if (val instanceof BlobRef) {
return [
{
ref: val,
path,
},
]
}
// retain cids & bytes
if (CID.asCID(val) || val instanceof Uint8Array) {
return []
}
return Object.entries(val).flatMap(([key, item]) =>
findBlobRefs(item, [...path, key], layer + 1),
)
}
// pass through
return []
}

const CONSTRAINTS = {
[lex.ids.AppBskyActorProfile]: {
avatar:
lex.schemaDict.AppBskyActorProfile.defs.main.record.properties.avatar,
banner:
lex.schemaDict.AppBskyActorProfile.defs.main.record.properties.banner,
},
[lex.ids.AppBskyFeedGenerator]: {
avatar:
lex.schemaDict.AppBskyFeedGenerator.defs.main.record.properties.avatar,
},
[lex.ids.AppBskyGraphList]: {
avatar: lex.schemaDict.AppBskyGraphList.defs.main.record.properties.avatar,
},
[lex.ids.AppBskyFeedPost]: {
'embed/images/image':
lex.schemaDict.AppBskyEmbedImages.defs.image.properties.image,
'embed/external/thumb':
lex.schemaDict.AppBskyEmbedExternal.defs.external.properties.thumb,
'embed/media/images/image':
lex.schemaDict.AppBskyEmbedImages.defs.image.properties.image,
'embed/media/external/thumb':
lex.schemaDict.AppBskyEmbedExternal.defs.external.properties.thumb,
},
}