Account declaration & invites ()

* schemas

* database & buffing up schemas

* declaration on createAccount, + fixing up test client

* fix up dev-env

* schema comments

* nsid for declaration actorType

* declaration description

* oops token schema slipped in

* declaration -> declarationCid

* missed a couple of db things
This commit is contained in:
Daniel Holmgren 2022-11-01 18:15:52 -05:00 committed by GitHub
parent 6bf748bc70
commit 2fb128d94f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1269 additions and 157 deletions

@ -22,12 +22,13 @@
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["accessJwt", "refreshJwt", "username", "did"],
"required": ["accessJwt", "refreshJwt", "username", "did", "declarationCid"],
"properties": {
"accessJwt": { "type": "string" },
"refreshJwt": { "type": "string" },
"username": { "type": "string" },
"did": { "type": "string" }
"did": { "type": "string" },
"declarationCid": { "type": "string" }
}
}
},

@ -0,0 +1,29 @@
{
"lexicon": 1,
"id": "app.bsky.declaration",
"description": "Context for an account that is considered intrinsic to it and alters the fundamental understanding of an account of changed. A declaration should be treated as immutable.",
"type": "record",
"key": "literal:self",
"record": {
"type": "object",
"required": ["actorType"],
"properties": {
"actorType": {
"oneOf": [
{"$ref": "#/defs/actorKnown"},
{"$ref": "#/defs/actorUnknown"}
]
}
}
},
"defs": {
"actorKnown": {
"type": "string",
"enum": ["app.bsky.actorUser", "app.bsky.actorScene"]
},
"actorUnknown": {
"type": "string",
"not": {"enum": ["app.bsky.actorUser", "app.bsky.actorScene"]}
}
}
}

@ -8,7 +8,14 @@
"type": "object",
"required": ["subject", "createdAt"],
"properties": {
"subject": { "type": "string" },
"subject": {
"type": "object",
"required": ["did", "declarationCid"],
"properties": {
"did": {"type": "string"},
"declarationCid": {"type": "string"}
}
},
"createdAt": {"type": "string", "format": "date-time"}
}
}

@ -41,7 +41,7 @@
},
"reason": {
"type": "string",
"$comment": "Expected values are 'like', 'repost', 'follow', 'badge', 'mention' and 'reply'."
"$comment": "Expected values are 'like', 'repost', 'follow', 'badge', 'invite', 'mention' and 'reply'."
},
"reasonSubject": {"type": "string"},
"record": {"type": "object"},

@ -0,0 +1,22 @@
{
"lexicon": 1,
"id": "app.bsky.invite",
"type": "record",
"key": "tid",
"record": {
"type": "object",
"required": ["group", "subject", "createdAt"],
"properties": {
"group": {"type": "string"},
"subject": {
"type": "object",
"required": ["did", "declarationCid"],
"properties": {
"did": {"type": "string"},
"declarationCid": {"type": "string"}
}
},
"createdAt": {"type": "string", "format": "date-time"}
}
}
}

@ -0,0 +1,29 @@
{
"lexicon": 1,
"id": "app.bsky.inviteAccept",
"type": "record",
"key": "tid",
"record": {
"type": "object",
"required": ["group", "invite", "createdAt"],
"properties": {
"group": {
"type": "object",
"required": ["did", "declarationCid"],
"properties": {
"did": {"type": "string"},
"declarationCid": {"type": "string"}
}
},
"invite": {
"type": "object",
"required": ["uri", "cid"],
"properties": {
"uri": {"type": "string"},
"cid": {"type": "string"}
}
},
"createdAt": {"type": "string", "format": "date-time"}
}
}
}

@ -31,6 +31,7 @@ import * as ComAtprotoSyncUpdateRepo from './types/com/atproto/syncUpdateRepo'
import * as AppBskyBadge from './types/app/bsky/badge'
import * as AppBskyBadgeAccept from './types/app/bsky/badgeAccept'
import * as AppBskyBadgeOffer from './types/app/bsky/badgeOffer'
import * as AppBskyDeclaration from './types/app/bsky/declaration'
import * as AppBskyFollow from './types/app/bsky/follow'
import * as AppBskyGetAuthorFeed from './types/app/bsky/getAuthorFeed'
import * as AppBskyGetBadgeMembers from './types/app/bsky/getBadgeMembers'
@ -45,6 +46,8 @@ import * as AppBskyGetUserFollowers from './types/app/bsky/getUserFollowers'
import * as AppBskyGetUserFollows from './types/app/bsky/getUserFollows'
import * as AppBskyGetUsersSearch from './types/app/bsky/getUsersSearch'
import * as AppBskyGetUsersTypeahead from './types/app/bsky/getUsersTypeahead'
import * as AppBskyInvite from './types/app/bsky/invite'
import * as AppBskyInviteAccept from './types/app/bsky/inviteAccept'
import * as AppBskyLike from './types/app/bsky/like'
import * as AppBskyMediaEmbed from './types/app/bsky/mediaEmbed'
import * as AppBskyPost from './types/app/bsky/post'
@ -78,6 +81,7 @@ export * as ComAtprotoSyncUpdateRepo from './types/com/atproto/syncUpdateRepo'
export * as AppBskyBadge from './types/app/bsky/badge'
export * as AppBskyBadgeAccept from './types/app/bsky/badgeAccept'
export * as AppBskyBadgeOffer from './types/app/bsky/badgeOffer'
export * as AppBskyDeclaration from './types/app/bsky/declaration'
export * as AppBskyFollow from './types/app/bsky/follow'
export * as AppBskyGetAuthorFeed from './types/app/bsky/getAuthorFeed'
export * as AppBskyGetBadgeMembers from './types/app/bsky/getBadgeMembers'
@ -92,6 +96,8 @@ export * as AppBskyGetUserFollowers from './types/app/bsky/getUserFollowers'
export * as AppBskyGetUserFollows from './types/app/bsky/getUserFollows'
export * as AppBskyGetUsersSearch from './types/app/bsky/getUsersSearch'
export * as AppBskyGetUsersTypeahead from './types/app/bsky/getUsersTypeahead'
export * as AppBskyInvite from './types/app/bsky/invite'
export * as AppBskyInviteAccept from './types/app/bsky/inviteAccept'
export * as AppBskyLike from './types/app/bsky/like'
export * as AppBskyMediaEmbed from './types/app/bsky/mediaEmbed'
export * as AppBskyPost from './types/app/bsky/post'
@ -430,7 +436,10 @@ export class BskyNS {
badge: BadgeRecord
badgeAccept: BadgeAcceptRecord
badgeOffer: BadgeOfferRecord
declaration: DeclarationRecord
follow: FollowRecord
invite: InviteRecord
inviteAccept: InviteAcceptRecord
like: LikeRecord
mediaEmbed: MediaEmbedRecord
post: PostRecord
@ -442,7 +451,10 @@ export class BskyNS {
this.badge = new BadgeRecord(service)
this.badgeAccept = new BadgeAcceptRecord(service)
this.badgeOffer = new BadgeOfferRecord(service)
this.declaration = new DeclarationRecord(service)
this.follow = new FollowRecord(service)
this.invite = new InviteRecord(service)
this.inviteAccept = new InviteAcceptRecord(service)
this.like = new LikeRecord(service)
this.mediaEmbed = new MediaEmbedRecord(service)
this.post = new PostRecord(service)
@ -805,6 +817,64 @@ export class BadgeOfferRecord {
}
}
export class DeclarationRecord {
_service: ServiceClient
constructor(service: ServiceClient) {
this._service = service
}
async list(
params: Omit<ComAtprotoRepoListRecords.QueryParams, 'collection'>
): Promise<{
cursor?: string,
records: { uri: string, value: AppBskyDeclaration.Record }[],
}> {
const res = await this._service.xrpc.call('com.atproto.repoListRecords', {
collection: 'app.bsky.declaration',
...params,
})
return res.data
}
async get(
params: Omit<ComAtprotoRepoGetRecord.QueryParams, 'collection'>
): Promise<{ uri: string, cid: string, value: AppBskyDeclaration.Record }> {
const res = await this._service.xrpc.call('com.atproto.repoGetRecord', {
collection: 'app.bsky.declaration',
...params,
})
return res.data
}
async create(
params: Omit<ComAtprotoRepoCreateRecord.QueryParams, 'collection'>,
record: AppBskyDeclaration.Record,
headers?: Record<string, string>
): Promise<{ uri: string, cid: string }> {
record.$type = 'app.bsky.declaration'
const res = await this._service.xrpc.call(
'com.atproto.repoCreateRecord',
{ collection: 'app.bsky.declaration', ...params },
record,
{ encoding: 'application/json', headers }
)
return res.data
}
async delete(
params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>,
headers?: Record<string, string>
): Promise<void> {
await this._service.xrpc.call(
'com.atproto.repoDeleteRecord',
{ collection: 'app.bsky.declaration', ...params },
undefined,
{ headers }
)
}
}
export class FollowRecord {
_service: ServiceClient
@ -863,6 +933,122 @@ export class FollowRecord {
}
}
export class InviteRecord {
_service: ServiceClient
constructor(service: ServiceClient) {
this._service = service
}
async list(
params: Omit<ComAtprotoRepoListRecords.QueryParams, 'collection'>
): Promise<{
cursor?: string,
records: { uri: string, value: AppBskyInvite.Record }[],
}> {
const res = await this._service.xrpc.call('com.atproto.repoListRecords', {
collection: 'app.bsky.invite',
...params,
})
return res.data
}
async get(
params: Omit<ComAtprotoRepoGetRecord.QueryParams, 'collection'>
): Promise<{ uri: string, cid: string, value: AppBskyInvite.Record }> {
const res = await this._service.xrpc.call('com.atproto.repoGetRecord', {
collection: 'app.bsky.invite',
...params,
})
return res.data
}
async create(
params: Omit<ComAtprotoRepoCreateRecord.QueryParams, 'collection'>,
record: AppBskyInvite.Record,
headers?: Record<string, string>
): Promise<{ uri: string, cid: string }> {
record.$type = 'app.bsky.invite'
const res = await this._service.xrpc.call(
'com.atproto.repoCreateRecord',
{ collection: 'app.bsky.invite', ...params },
record,
{ encoding: 'application/json', headers }
)
return res.data
}
async delete(
params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>,
headers?: Record<string, string>
): Promise<void> {
await this._service.xrpc.call(
'com.atproto.repoDeleteRecord',
{ collection: 'app.bsky.invite', ...params },
undefined,
{ headers }
)
}
}
export class InviteAcceptRecord {
_service: ServiceClient
constructor(service: ServiceClient) {
this._service = service
}
async list(
params: Omit<ComAtprotoRepoListRecords.QueryParams, 'collection'>
): Promise<{
cursor?: string,
records: { uri: string, value: AppBskyInviteAccept.Record }[],
}> {
const res = await this._service.xrpc.call('com.atproto.repoListRecords', {
collection: 'app.bsky.inviteAccept',
...params,
})
return res.data
}
async get(
params: Omit<ComAtprotoRepoGetRecord.QueryParams, 'collection'>
): Promise<{ uri: string, cid: string, value: AppBskyInviteAccept.Record }> {
const res = await this._service.xrpc.call('com.atproto.repoGetRecord', {
collection: 'app.bsky.inviteAccept',
...params,
})
return res.data
}
async create(
params: Omit<ComAtprotoRepoCreateRecord.QueryParams, 'collection'>,
record: AppBskyInviteAccept.Record,
headers?: Record<string, string>
): Promise<{ uri: string, cid: string }> {
record.$type = 'app.bsky.inviteAccept'
const res = await this._service.xrpc.call(
'com.atproto.repoCreateRecord',
{ collection: 'app.bsky.inviteAccept', ...params },
record,
{ encoding: 'application/json', headers }
)
return res.data
}
async delete(
params: Omit<ComAtprotoRepoDeleteRecord.QueryParams, 'collection'>,
headers?: Record<string, string>
): Promise<void> {
await this._service.xrpc.call(
'com.atproto.repoDeleteRecord',
{ collection: 'app.bsky.inviteAccept', ...params },
undefined,
{ headers }
)
}
}
export class LikeRecord {
_service: ServiceClient

@ -39,7 +39,13 @@ export const methodSchemaDict: Record<string, MethodSchema> = {
encoding: 'application/json',
schema: {
type: 'object',
required: ['accessJwt', 'refreshJwt', 'username', 'did'],
required: [
'accessJwt',
'refreshJwt',
'username',
'did',
'declarationCid',
],
properties: {
accessJwt: {
type: 'string',
@ -53,6 +59,9 @@ export const methodSchemaDict: Record<string, MethodSchema> = {
did: {
type: 'string',
},
declarationCid: {
type: 'string',
},
},
$defs: {},
},
@ -1629,7 +1638,7 @@ export const methodSchemaDict: Record<string, MethodSchema> = {
reason: {
type: 'string',
$comment:
"Expected values are 'like', 'repost', 'follow', 'badge', 'mention' and 'reply'.",
"Expected values are 'like', 'repost', 'follow', 'badge', 'invite', 'mention' and 'reply'.",
},
reasonSubject: {
type: 'string',
@ -1688,7 +1697,7 @@ export const methodSchemaDict: Record<string, MethodSchema> = {
reason: {
type: 'string',
$comment:
"Expected values are 'like', 'repost', 'follow', 'badge', 'mention' and 'reply'.",
"Expected values are 'like', 'repost', 'follow', 'badge', 'invite', 'mention' and 'reply'.",
},
reasonSubject: {
type: 'string',
@ -2592,7 +2601,10 @@ export const ids = {
AppBskyBadge: 'app.bsky.badge',
AppBskyBadgeAccept: 'app.bsky.badgeAccept',
AppBskyBadgeOffer: 'app.bsky.badgeOffer',
AppBskyDeclaration: 'app.bsky.declaration',
AppBskyFollow: 'app.bsky.follow',
AppBskyInvite: 'app.bsky.invite',
AppBskyInviteAccept: 'app.bsky.inviteAccept',
AppBskyLike: 'app.bsky.like',
AppBskyMediaEmbed: 'app.bsky.mediaEmbed',
AppBskyPost: 'app.bsky.post',
@ -2823,6 +2835,54 @@ export const recordSchemaDict: Record<string, RecordSchema> = {
},
},
},
'app.bsky.declaration': {
lexicon: 1,
id: 'app.bsky.declaration',
description:
'Context for an account that is considered intrinsic to it and alters the fundamental understanding of an account of changed. A declaration should be treated as immutable.',
type: 'record',
key: 'literal:self',
record: {
type: 'object',
required: ['actorType'],
properties: {
actorType: {
oneOf: [
{
$ref: '#/$defs/actorKnown',
},
{
$ref: '#/$defs/actorUnknown',
},
],
},
},
$defs: {
actorKnown: {
type: 'string',
enum: ['app.bsky.actorUser', 'app.bsky.actorScene'],
},
actorUnknown: {
type: 'string',
not: {
enum: ['app.bsky.actorUser', 'app.bsky.actorScene'],
},
},
},
},
defs: {
actorKnown: {
type: 'string',
enum: ['app.bsky.actorUser', 'app.bsky.actorScene'],
},
actorUnknown: {
type: 'string',
not: {
enum: ['app.bsky.actorUser', 'app.bsky.actorScene'],
},
},
},
},
'app.bsky.follow': {
lexicon: 1,
id: 'app.bsky.follow',
@ -2834,7 +2894,89 @@ export const recordSchemaDict: Record<string, RecordSchema> = {
required: ['subject', 'createdAt'],
properties: {
subject: {
type: 'object',
required: ['did', 'declarationCid'],
properties: {
did: {
type: 'string',
},
declarationCid: {
type: 'string',
},
},
},
createdAt: {
type: 'string',
format: 'date-time',
},
},
$defs: {},
},
},
'app.bsky.invite': {
lexicon: 1,
id: 'app.bsky.invite',
type: 'record',
key: 'tid',
record: {
type: 'object',
required: ['group', 'subject', 'createdAt'],
properties: {
group: {
type: 'string',
},
subject: {
type: 'object',
required: ['did', 'declarationCid'],
properties: {
did: {
type: 'string',
},
declarationCid: {
type: 'string',
},
},
},
createdAt: {
type: 'string',
format: 'date-time',
},
},
$defs: {},
},
},
'app.bsky.inviteAccept': {
lexicon: 1,
id: 'app.bsky.inviteAccept',
type: 'record',
key: 'tid',
record: {
type: 'object',
required: ['group', 'invite', 'createdAt'],
properties: {
group: {
type: 'object',
required: ['did', 'declarationCid'],
properties: {
did: {
type: 'string',
},
declarationCid: {
type: 'string',
},
},
},
invite: {
type: 'object',
required: ['uri', 'cid'],
properties: {
uri: {
type: 'string',
},
cid: {
type: 'string',
},
},
},
createdAt: {
type: 'string',

@ -0,0 +1,10 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
export type ActorKnown = 'app.bsky.actorUser' | 'app.bsky.actorScene'
export type ActorUnknown = string
export interface Record {
actorType: ActorKnown | ActorUnknown;
[k: string]: unknown;
}

@ -2,7 +2,11 @@
* GENERATED CODE - DO NOT MODIFY
*/
export interface Record {
subject: string;
subject: {
did: string,
declarationCid: string,
[k: string]: unknown,
};
createdAt: string;
[k: string]: unknown;
}

@ -0,0 +1,13 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
export interface Record {
group: string;
subject: {
did: string,
declarationCid: string,
[k: string]: unknown,
};
createdAt: string;
[k: string]: unknown;
}

@ -0,0 +1,17 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
export interface Record {
group: {
did: string,
declarationCid: string,
[k: string]: unknown,
};
invite: {
uri: string,
cid: string,
[k: string]: unknown,
};
createdAt: string;
[k: string]: unknown;
}

@ -23,6 +23,7 @@ export interface OutputSchema {
refreshJwt: string;
username: string;
did: string;
declarationCid: string;
}
export interface Response {

@ -47,6 +47,7 @@ export async function generateMockSetup(env: DevEnv) {
interface User {
email: string
did: string
declarationCid: string
username: string
password: string
api: ServiceClient
@ -55,6 +56,7 @@ export async function generateMockSetup(env: DevEnv) {
{
email: 'alice@test.com',
did: '',
declarationCid: '',
username: `alice.test`,
password: 'hunter2',
api: clients.alice,
@ -62,6 +64,7 @@ export async function generateMockSetup(env: DevEnv) {
{
email: 'bob@test.com',
did: '',
declarationCid: '',
username: `bob.test`,
password: 'hunter2',
api: clients.bob,
@ -69,6 +72,7 @@ export async function generateMockSetup(env: DevEnv) {
{
email: 'carla@test.com',
did: '',
declarationCid: '',
username: `carla.test`,
password: 'hunter2',
api: clients.carla,
@ -85,6 +89,7 @@ export async function generateMockSetup(env: DevEnv) {
{ email: user.email, username: user.username, password: user.password },
)
user.did = res.data.did
user.declarationCid = res.data.declarationCid
user.api.setHeader('Authorization', `Bearer ${res.data.accessJwt}`)
await user.api.app.bsky.profile.create(
{ did: user.did },
@ -96,21 +101,24 @@ export async function generateMockSetup(env: DevEnv) {
}
// everybody follows everybody
const follow = async (author: User, subject: string) => {
const follow = async (author: User, subject: User) => {
await author.api.app.bsky.follow.create(
{ did: author.did },
{
subject,
subject: {
did: subject.did,
declarationCid: subject.declarationCid,
},
createdAt: date.next().value,
},
)
}
await follow(alice, bob.did)
await follow(alice, carla.did)
await follow(bob, alice.did)
await follow(bob, carla.did)
await follow(carla, alice.did)
await follow(carla, bob.did)
await follow(alice, bob)
await follow(alice, carla)
await follow(bob, alice)
await follow(bob, carla)
await follow(carla, alice)
await follow(carla, bob)
// a set of posts and reposts
const posts: { uri: string; cid: string }[] = []

@ -55,7 +55,7 @@ export default function (server: Server) {
// Followee's posts and reposts, and requester's posts
const followingIdsSubquery = db.db
.selectFrom('app_bsky_follow as follow')
.select('follow.subject')
.select('follow.subjectDid')
.where('follow.creator', '=', requester)
repostsQb = repostsQb
.where('creator', '!=', requester)

@ -38,7 +38,7 @@ export default function (server: Server) {
.as('followsCount'),
db.db
.selectFrom('app_bsky_follow')
.whereRef('subject', '=', ref('user_did.did'))
.whereRef('subjectDid', '=', ref('user_did.did'))
.select(countAll.as('count'))
.as('followersCount'),
db.db
@ -49,7 +49,7 @@ export default function (server: Server) {
db.db
.selectFrom('app_bsky_follow')
.where('creator', '=', requester)
.whereRef('subject', '=', ref('user_did.did'))
.whereRef('subjectDid', '=', ref('user_did.did'))
.select('uri')
.as('requesterFollow'),
])

@ -18,7 +18,7 @@ export default function (server: Server) {
let followersReq = db.db
.selectFrom('app_bsky_follow as follow')
.where('follow.subject', '=', subject.did)
.where('follow.subjectDid', '=', subject.did)
.innerJoin('user_did as creator', 'creator.did', 'follow.creator')
.leftJoin(
'app_bsky_profile as profile',

@ -19,11 +19,11 @@ export default function (server: Server) {
let followsReq = db.db
.selectFrom('app_bsky_follow as follow')
.where('follow.creator', '=', creator.did)
.innerJoin('user_did as subject', 'subject.did', 'follow.subject')
.innerJoin('user_did as subject', 'subject.did', 'follow.subjectDid')
.leftJoin(
'app_bsky_profile as profile',
'profile.creator',
'follow.subject',
'follow.subjectDid',
)
.select([
'subject.did as did',

@ -1,5 +1,5 @@
import { InvalidRequestError } from '@atproto/xrpc-server'
import { Repo } from '@atproto/repo'
import { RepoStructure } from '@atproto/repo'
import { PlcClient } from '@atproto/plc'
import { Server } from '../../../lexicon'
import * as locals from '../../../locals'
@ -8,6 +8,8 @@ import { UserAlreadyExistsError } from '../../../db'
import SqlBlockstore from '../../../sql-blockstore'
import { ensureUsernameValid } from './util/username'
import { grantRefreshToken } from './util/auth'
import { AtUri } from '@atproto/uri'
import * as schema from '../../../lexicon/schemas'
export default function (server: Server) {
server.com.atproto.getAccountsConfig((_params, _input, _req, res) => {
@ -124,22 +126,46 @@ export default function (server: Server) {
// Setup repo root
const authStore = locals.getAuthstore(res, did)
const blockstore = new SqlBlockstore(dbTxn, did, now)
const repo = await Repo.create(blockstore, did, authStore)
const repo = await RepoStructure.create(blockstore, did, authStore)
await dbTxn.db
.insertInto('repo_root')
.values({
did: did,
root: repo.cid.toString(),
indexedAt: now,
const declaration = {
$type: 'app.bsky.declaration',
actorType: 'app.bsky.actorUser',
}
const declarationCid = await blockstore.put(declaration)
const uri = new AtUri(`${did}/${schema.ids.AppBskyDeclaration}/self`)
await repo
.stageUpdate({
action: 'create',
collection: uri.collection,
rkey: uri.rkey,
cid: declarationCid,
})
.execute()
.createCommit(authStore, async (_prev, curr) => {
await dbTxn.db
.insertInto('repo_root')
.values({
did: did,
root: curr.toString(),
indexedAt: now,
})
.execute()
return null
})
await dbTxn.indexRecord(uri, declarationCid, declaration, now)
const access = auth.createAccessToken(did)
const refresh = auth.createRefreshToken(did)
await grantRefreshToken(dbTxn, refresh.payload)
return { did, accessJwt: access.jwt, refreshJwt: refresh.jwt }
return {
did,
declarationCid,
accessJwt: access.jwt,
refreshJwt: refresh.jwt,
}
})
return {
@ -149,6 +175,7 @@ export default function (server: Server) {
did: result.did,
accessJwt: result.accessJwt,
refreshJwt: result.refreshJwt,
declarationCid: result.declarationCid.toString(),
},
}
})

@ -5,16 +5,19 @@ import * as refreshToken from './tables/refresh-token'
import * as record from './tables/record'
import * as ipldBlock from './tables/ipld-block'
import * as ipldBlockCreator from './tables/ipld-block-creator'
import * as invite from './tables/invite'
import * as inviteCode from './tables/invite-code'
import * as notification from './tables/user-notification'
import * as declaration from './records/declaration'
import * as profile from './records/profile'
import * as post from './records/post'
import * as like from './records/like'
import * as repost from './records/repost'
import * as follow from './records/follow'
import * as profile from './records/profile'
import * as badge from './records/badge'
import * as badgeAccept from './records/badgeAccept'
import * as badgeOffer from './records/badgeOffer'
import * as invite from './records/invite'
import * as inviteAccept from './records/inviteAccept'
export type DatabaseSchema = user.PartialDB &
userDid.PartialDB &
@ -23,13 +26,16 @@ export type DatabaseSchema = user.PartialDB &
record.PartialDB &
ipldBlock.PartialDB &
ipldBlockCreator.PartialDB &
invite.PartialDB &
inviteCode.PartialDB &
notification.PartialDB &
declaration.PartialDB &
profile.PartialDB &
post.PartialDB &
like.PartialDB &
repost.PartialDB &
follow.PartialDB &
profile.PartialDB &
badge.PartialDB &
badgeAccept.PartialDB &
badgeOffer.PartialDB
badgeOffer.PartialDB &
invite.PartialDB &
inviteAccept.PartialDB

@ -4,6 +4,9 @@ import SqliteDB from 'better-sqlite3'
import { Pool as PgPool, types as pgTypes } from 'pg'
import { ValidationResult, ValidationResultCode } from '@atproto/lexicon'
import { DbRecordPlugin, NotificationsPlugin } from './types'
import * as Declaration from '../lexicon/types/app/bsky/declaration'
import * as Invite from '../lexicon/types/app/bsky/invite'
import * as InviteAccept from '../lexicon/types/app/bsky/inviteAccept'
import * as Badge from '../lexicon/types/app/bsky/badge'
import * as BadgeAccept from '../lexicon/types/app/bsky/badgeAccept'
import * as BadgeOffer from '../lexicon/types/app/bsky/badgeOffer'
@ -12,10 +15,13 @@ import * as Like from '../lexicon/types/app/bsky/like'
import * as Post from '../lexicon/types/app/bsky/post'
import * as Profile from '../lexicon/types/app/bsky/profile'
import * as Repost from '../lexicon/types/app/bsky/repost'
import declarationPlugin, { AppBskyDeclaration } from './records/declaration'
import postPlugin, { AppBskyPost } from './records/post'
import likePlugin, { AppBskyLike } from './records/like'
import repostPlugin, { AppBskyRepost } from './records/repost'
import followPlugin, { AppBskyFollow } from './records/follow'
import invitePlugin, { AppBskyInvite } from './records/invite'
import inviteAcceptPlugin, { AppBskyInviteAccept } from './records/inviteAccept'
import badgePlugin, { AppBskyBadge } from './records/badge'
import badgeAcceptPlugin, { AppBskyBadgeAccept } from './records/badgeAccept'
import badgeOfferPlugin, { AppBskyBadgeOffer } from './records/badgeOffer'
@ -36,11 +42,14 @@ import { UserDid } from './tables/user-did'
export class Database {
migrator: Migrator
records: {
declaration: DbRecordPlugin<Declaration.Record, AppBskyDeclaration>
post: DbRecordPlugin<Post.Record, AppBskyPost>
like: DbRecordPlugin<Like.Record, AppBskyLike>
repost: DbRecordPlugin<Repost.Record, AppBskyRepost>
follow: DbRecordPlugin<Follow.Record, AppBskyFollow>
profile: DbRecordPlugin<Profile.Record, AppBskyProfile>
invite: DbRecordPlugin<Invite.Record, AppBskyInvite>
inviteAccept: DbRecordPlugin<InviteAccept.Record, AppBskyInviteAccept>
badge: DbRecordPlugin<Badge.Record, AppBskyBadge>
badgeAccept: DbRecordPlugin<BadgeAccept.Record, AppBskyBadgeAccept>
badgeOffer: DbRecordPlugin<BadgeOffer.Record, AppBskyBadgeOffer>
@ -53,10 +62,13 @@ export class Database {
public schema?: string,
) {
this.records = {
declaration: declarationPlugin(db),
post: postPlugin(db),
like: likePlugin(db),
repost: repostPlugin(db),
follow: followPlugin(db),
invite: invitePlugin(db),
inviteAccept: inviteAcceptPlugin(db),
badge: badgePlugin(db),
badgeAccept: badgeAcceptPlugin(db),
badgeOffer: badgeOfferPlugin(db),

@ -8,14 +8,17 @@ const repoRootTable = 'repo_root'
const recordTable = 'record'
const ipldBlockTable = 'ipld_block'
const ipldBlockCreatorTable = 'ipld_block_creator'
const inviteTable = 'invite_code'
const inviteCodeTable = 'invite_code'
const inviteUseTable = 'invite_code_use'
const notificationTable = 'user_notification'
const declarationTable = 'app_bsky_declaration'
const profileTable = 'app_bsky_profile'
const profileBadgeTable = 'app_bsky_profile_badge'
const badgeTable = 'app_bsky_badge'
const badgeOfferTable = 'app_bsky_badge_offer'
const badgeAcceptTable = 'app_bsky_badge_accept'
const inviteTable = 'app_bsky_invite'
const inviteAcceptTable = 'app_bsky_invite_accept'
const followTable = 'app_bsky_follow'
const postTable = 'app_bsky_post'
const postEntityTable = 'app_bsky_post_entity'
@ -113,9 +116,9 @@ export async function up(db: Kysely<unknown>, dialect: Dialect): Promise<void> {
.addColumn('did', 'varchar', (col) => col.notNull())
.addPrimaryKeyConstraint(`${ipldBlockCreatorTable}_pkey`, ['cid', 'did'])
.execute()
// Invites
// Invite Codes
await db.schema
.createTable(inviteTable)
.createTable(inviteCodeTable)
.addColumn('code', 'varchar', (col) => col.primaryKey())
.addColumn('availableUses', 'integer', (col) => col.notNull())
.addColumn('disabled', 'int2', (col) => col.defaultTo(0))
@ -142,6 +145,15 @@ export async function up(db: Kysely<unknown>, dialect: Dialect): Promise<void> {
.addColumn('reasonSubject', 'varchar')
.addColumn('indexedAt', 'varchar', (col) => col.notNull())
.execute()
// Declarations
await db.schema
.createTable(declarationTable)
.addColumn('uri', 'varchar', (col) => col.primaryKey())
.addColumn('cid', 'varchar', (col) => col.notNull())
.addColumn('creator', 'varchar', (col) => col.notNull())
.addColumn('actorType', 'varchar', (col) => col.notNull())
.addColumn('indexedAt', 'varchar', (col) => col.notNull())
.execute()
// Profiles
await db.schema
.createTable(profileTable)
@ -205,13 +217,38 @@ export async function up(db: Kysely<unknown>, dialect: Dialect): Promise<void> {
.addColumn('createdAt', 'varchar', (col) => col.notNull())
.addColumn('indexedAt', 'varchar', (col) => col.notNull())
.execute()
// Invites (Records)
await db.schema
.createTable(inviteTable)
.addColumn('uri', 'varchar', (col) => col.primaryKey())
.addColumn('cid', 'varchar', (col) => col.notNull())
.addColumn('creator', 'varchar', (col) => col.notNull())
.addColumn('group', 'varchar', (col) => col.notNull())
.addColumn('subjectDid', 'varchar', (col) => col.notNull())
.addColumn('subjectDeclarationCid', 'varchar', (col) => col.notNull())
.addColumn('createdAt', 'varchar', (col) => col.notNull())
.addColumn('indexedAt', 'varchar', (col) => col.notNull())
.execute()
await db.schema
.createTable(inviteAcceptTable)
.addColumn('uri', 'varchar', (col) => col.primaryKey())
.addColumn('cid', 'varchar', (col) => col.notNull())
.addColumn('creator', 'varchar', (col) => col.notNull())
.addColumn('groupDid', 'varchar', (col) => col.notNull())
.addColumn('groupDeclarationCid', 'varchar', (col) => col.notNull())
.addColumn('inviteUri', 'varchar', (col) => col.notNull())
.addColumn('inviteCid', 'varchar', (col) => col.notNull())
.addColumn('createdAt', 'varchar', (col) => col.notNull())
.addColumn('indexedAt', 'varchar', (col) => col.notNull())
.execute()
// Follows
await db.schema
.createTable(followTable)
.addColumn('uri', 'varchar', (col) => col.primaryKey())
.addColumn('cid', 'varchar', (col) => col.notNull())
.addColumn('creator', 'varchar', (col) => col.notNull())
.addColumn('subject', 'varchar', (col) => col.notNull())
.addColumn('subjectDid', 'varchar', (col) => col.notNull())
.addColumn('subjectDeclarationCid', 'varchar', (col) => col.notNull())
.addColumn('createdAt', 'varchar', (col) => col.notNull())
.addColumn('indexedAt', 'varchar', (col) => col.notNull())
.execute()
@ -265,14 +302,17 @@ export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable(postEntityTable).execute()
await db.schema.dropTable(postTable).execute()
await db.schema.dropTable(followTable).execute()
await db.schema.dropTable(inviteAcceptTable).execute()
await db.schema.dropTable(inviteTable).execute()
await db.schema.dropTable(badgeAcceptTable).execute()
await db.schema.dropTable(badgeOfferTable).execute()
await db.schema.dropTable(badgeTable).execute()
await db.schema.dropTable(profileBadgeTable).execute()
await db.schema.dropTable(profileTable).execute()
await db.schema.dropTable(declarationTable).execute()
await db.schema.dropTable(notificationTable).execute()
await db.schema.dropTable(inviteUseTable).execute()
await db.schema.dropTable(inviteTable).execute()
await db.schema.dropTable(inviteCodeTable).execute()
await db.schema.dropTable(ipldBlockCreatorTable).execute()
await db.schema.dropTable(ipldBlockTable).execute()
await db.schema.dropTable(recordTable).execute()

@ -0,0 +1,97 @@
import { Kysely } from 'kysely'
import { AtUri } from '@atproto/uri'
import { CID } from 'multiformats/cid'
import * as Declaration from '../../lexicon/types/app/bsky/declaration'
import { DbRecordPlugin, Notification } from '../types'
import * as schemas from '../schemas'
const type = schemas.ids.AppBskyDeclaration
const tableName = 'app_bsky_declaration'
export interface AppBskyDeclaration {
uri: string
cid: string
creator: string
actorType: string
indexedAt: string
}
export type PartialDB = { [tableName]: AppBskyDeclaration }
const validator = schemas.records.createRecordValidator(type)
const matchesSchema = (obj: unknown): obj is Declaration.Record => {
return validator.isValid(obj)
}
const validateSchema = (obj: unknown) => validator.validate(obj)
const translateDbObj = (dbObj: AppBskyDeclaration): Declaration.Record => {
return {
actorType: dbObj.actorType,
}
}
const getFn =
(db: Kysely<PartialDB>) =>
async (uri: AtUri): Promise<Declaration.Record | null> => {
const found = await db
.selectFrom(tableName)
.selectAll()
.where('uri', '=', uri.toString())
.executeTakeFirst()
return !found ? null : translateDbObj(found)
}
const insertFn =
(db: Kysely<PartialDB>) =>
async (
uri: AtUri,
cid: CID,
obj: unknown,
timestamp?: string,
): Promise<void> => {
if (!matchesSchema(obj)) {
throw new Error(`Record does not match schema: ${type}`)
}
await db
.insertInto(tableName)
.values({
uri: uri.toString(),
cid: cid.toString(),
creator: uri.host,
actorType: obj.actorType,
indexedAt: timestamp || new Date().toISOString(),
})
.execute()
}
const deleteFn =
(db: Kysely<PartialDB>) =>
async (uri: AtUri): Promise<void> => {
await db.deleteFrom(tableName).where('uri', '=', uri.toString()).execute()
}
const notifsForRecord = (
_uri: AtUri,
_cid: CID,
_obj: unknown,
): Notification[] => {
return []
}
export const makePlugin = (
db: Kysely<PartialDB>,
): DbRecordPlugin<Declaration.Record, AppBskyDeclaration> => {
return {
collection: type,
tableName,
validateSchema,
matchesSchema,
translateDbObj,
get: getFn(db),
insert: insertFn(db),
delete: deleteFn(db),
notifsForRecord,
}
}
export default makePlugin

@ -11,7 +11,8 @@ export interface AppBskyFollow {
uri: string
cid: string
creator: string
subject: string
subjectDid: string
subjectDeclarationCid: string
createdAt: string
indexedAt: string
}
@ -26,7 +27,10 @@ const validateSchema = (obj: unknown) => validator.validate(obj)
const translateDbObj = (dbObj: AppBskyFollow): Follow.Record => {
return {
subject: dbObj.subject,
subject: {
did: dbObj.subjectDid,
declarationCid: dbObj.subjectDeclarationCid,
},
createdAt: dbObj.createdAt,
}
}
@ -57,7 +61,8 @@ const insertFn =
uri: uri.toString(),
cid: cid.toString(),
creator: uri.host,
subject: obj.subject,
subjectDid: obj.subject.did,
subjectDeclarationCid: obj.subject.declarationCid,
createdAt: obj.createdAt,
indexedAt: timestamp || new Date().toISOString(),
}
@ -82,7 +87,7 @@ const notifsForRecord = (
throw new Error(`Record does not match schema: ${type}`)
}
const notif = {
userDid: obj.subject,
userDid: obj.subject.did,
author: uri.host,
recordUri: uri.toString(),
recordCid: cid.toString(),

@ -0,0 +1,119 @@
import { Kysely } from 'kysely'
import { AtUri } from '@atproto/uri'
import { CID } from 'multiformats/cid'
import * as Invite from '../../lexicon/types/app/bsky/invite'
import { DbRecordPlugin, Notification } from '../types'
import * as schemas from '../schemas'
const type = schemas.ids.AppBskyInvite
const tableName = 'app_bsky_invite'
export interface AppBskyInvite {
uri: string
cid: string
creator: string
group: string
subjectDid: string
subjectDeclarationCid: string
createdAt: string
indexedAt: string
}
export type PartialDB = { [tableName]: AppBskyInvite }
const validator = schemas.records.createRecordValidator(type)
const matchesSchema = (obj: unknown): obj is Invite.Record => {
return validator.isValid(obj)
}
const validateSchema = (obj: unknown) => validator.validate(obj)
const translateDbObj = (dbObj: AppBskyInvite): Invite.Record => {
return {
group: dbObj.group,
subject: {
did: dbObj.subjectDid,
declarationCid: dbObj.subjectDeclarationCid,
},
createdAt: dbObj.createdAt,
}
}
const getFn =
(db: Kysely<PartialDB>) =>
async (uri: AtUri): Promise<Invite.Record | null> => {
const found = await db
.selectFrom(tableName)
.selectAll()
.where('uri', '=', uri.toString())
.executeTakeFirst()
return !found ? null : translateDbObj(found)
}
const insertFn =
(db: Kysely<PartialDB>) =>
async (
uri: AtUri,
cid: CID,
obj: unknown,
timestamp?: string,
): Promise<void> => {
if (!matchesSchema(obj)) {
throw new Error(`Record does not match schema: ${type}`)
}
await db
.insertInto(tableName)
.values({
uri: uri.toString(),
cid: cid.toString(),
creator: uri.host,
group: obj.group,
subjectDid: obj.subject.did,
subjectDeclarationCid: obj.subject.declarationCid,
createdAt: obj.createdAt,
indexedAt: timestamp || new Date().toISOString(),
})
.execute()
}
const deleteFn =
(db: Kysely<PartialDB>) =>
async (uri: AtUri): Promise<void> => {
await db.deleteFrom(tableName).where('uri', '=', uri.toString()).execute()
}
const notifsForRecord = (
uri: AtUri,
cid: CID,
obj: unknown,
): Notification[] => {
if (!matchesSchema(obj)) {
throw new Error(`Record does not match schema: ${type}`)
}
const notif = {
userDid: obj.subject.did,
author: uri.host,
recordUri: uri.toString(),
recordCid: cid.toString(),
reason: 'invite',
reasonSubject: obj.group,
}
return [notif]
}
export const makePlugin = (
db: Kysely<PartialDB>,
): DbRecordPlugin<Invite.Record, AppBskyInvite> => {
return {
collection: type,
tableName,
validateSchema,
matchesSchema,
translateDbObj,
get: getFn(db),
insert: insertFn(db),
delete: deleteFn(db),
notifsForRecord,
}
}
export default makePlugin

@ -0,0 +1,113 @@
import { Kysely } from 'kysely'
import { AtUri } from '@atproto/uri'
import { CID } from 'multiformats/cid'
import * as InviteAccept from '../../lexicon/types/app/bsky/inviteAccept'
import { DbRecordPlugin, Notification } from '../types'
import * as schemas from '../schemas'
const type = schemas.ids.AppBskyInviteAccept
const tableName = 'app_bsky_invite_accept'
export interface AppBskyInviteAccept {
uri: string
cid: string
creator: string
groupDid: string
groupDeclarationCid: string
inviteUri: string
inviteCid: string
createdAt: string
indexedAt: string
}
export type PartialDB = { [tableName]: AppBskyInviteAccept }
const validator = schemas.records.createRecordValidator(type)
const matchesSchema = (obj: unknown): obj is InviteAccept.Record => {
return validator.isValid(obj)
}
const validateSchema = (obj: unknown) => validator.validate(obj)
const translateDbObj = (dbObj: AppBskyInviteAccept): InviteAccept.Record => {
return {
group: {
did: dbObj.groupDid,
declarationCid: dbObj.groupDeclarationCid,
},
invite: {
uri: dbObj.inviteUri,
cid: dbObj.inviteCid,
},
createdAt: dbObj.createdAt,
}
}
const getFn =
(db: Kysely<PartialDB>) =>
async (uri: AtUri): Promise<InviteAccept.Record | null> => {
const found = await db
.selectFrom(tableName)
.selectAll()
.where('uri', '=', uri.toString())
.executeTakeFirst()
return !found ? null : translateDbObj(found)
}
const insertFn =
(db: Kysely<PartialDB>) =>
async (
uri: AtUri,
cid: CID,
obj: unknown,
timestamp?: string,
): Promise<void> => {
if (!matchesSchema(obj)) {
throw new Error(`Record does not match schema: ${type}`)
}
await db
.insertInto(tableName)
.values({
uri: uri.toString(),
cid: cid.toString(),
creator: uri.host,
groupDid: obj.group.did,
groupDeclarationCid: obj.group.declarationCid,
inviteUri: obj.invite.uri,
inviteCid: obj.invite.cid,
createdAt: obj.createdAt,
indexedAt: timestamp || new Date().toISOString(),
})
.execute()
}
const deleteFn =
(db: Kysely<PartialDB>) =>
async (uri: AtUri): Promise<void> => {
await db.deleteFrom(tableName).where('uri', '=', uri.toString()).execute()
}
const notifsForRecord = (
_uri: AtUri,
_cid: CID,
_obj: unknown,
): Notification[] => {
return []
}
export const makePlugin = (
db: Kysely<PartialDB>,
): DbRecordPlugin<InviteAccept.Record, AppBskyInviteAccept> => {
return {
collection: type,
tableName,
validateSchema,
matchesSchema,
translateDbObj,
get: getFn(db),
insert: insertFn(db),
delete: deleteFn(db),
notifsForRecord,
}
}
export default makePlugin

@ -38,5 +38,6 @@ export type NotificationReason =
| 'repost'
| 'follow'
| 'badge'
| 'invite'
| 'mention'
| 'reply'

@ -39,7 +39,13 @@ export const methodSchemaDict: Record<string, MethodSchema> = {
encoding: 'application/json',
schema: {
type: 'object',
required: ['accessJwt', 'refreshJwt', 'username', 'did'],
required: [
'accessJwt',
'refreshJwt',
'username',
'did',
'declarationCid',
],
properties: {
accessJwt: {
type: 'string',
@ -53,6 +59,9 @@ export const methodSchemaDict: Record<string, MethodSchema> = {
did: {
type: 'string',
},
declarationCid: {
type: 'string',
},
},
$defs: {},
},
@ -1629,7 +1638,7 @@ export const methodSchemaDict: Record<string, MethodSchema> = {
reason: {
type: 'string',
$comment:
"Expected values are 'like', 'repost', 'follow', 'badge', 'mention' and 'reply'.",
"Expected values are 'like', 'repost', 'follow', 'badge', 'invite', 'mention' and 'reply'.",
},
reasonSubject: {
type: 'string',
@ -1688,7 +1697,7 @@ export const methodSchemaDict: Record<string, MethodSchema> = {
reason: {
type: 'string',
$comment:
"Expected values are 'like', 'repost', 'follow', 'badge', 'mention' and 'reply'.",
"Expected values are 'like', 'repost', 'follow', 'badge', 'invite', 'mention' and 'reply'.",
},
reasonSubject: {
type: 'string',
@ -2592,7 +2601,10 @@ export const ids = {
AppBskyBadge: 'app.bsky.badge',
AppBskyBadgeAccept: 'app.bsky.badgeAccept',
AppBskyBadgeOffer: 'app.bsky.badgeOffer',
AppBskyDeclaration: 'app.bsky.declaration',
AppBskyFollow: 'app.bsky.follow',
AppBskyInvite: 'app.bsky.invite',
AppBskyInviteAccept: 'app.bsky.inviteAccept',
AppBskyLike: 'app.bsky.like',
AppBskyMediaEmbed: 'app.bsky.mediaEmbed',
AppBskyPost: 'app.bsky.post',
@ -2823,6 +2835,54 @@ export const recordSchemaDict: Record<string, RecordSchema> = {
},
},
},
'app.bsky.declaration': {
lexicon: 1,
id: 'app.bsky.declaration',
description:
'Context for an account that is considered intrinsic to it and alters the fundamental understanding of an account of changed. A declaration should be treated as immutable.',
type: 'record',
key: 'literal:self',
record: {
type: 'object',
required: ['actorType'],
properties: {
actorType: {
oneOf: [
{
$ref: '#/$defs/actorKnown',
},
{
$ref: '#/$defs/actorUnknown',
},
],
},
},
$defs: {
actorKnown: {
type: 'string',
enum: ['app.bsky.actorUser', 'app.bsky.actorScene'],
},
actorUnknown: {
type: 'string',
not: {
enum: ['app.bsky.actorUser', 'app.bsky.actorScene'],
},
},
},
},
defs: {
actorKnown: {
type: 'string',
enum: ['app.bsky.actorUser', 'app.bsky.actorScene'],
},
actorUnknown: {
type: 'string',
not: {
enum: ['app.bsky.actorUser', 'app.bsky.actorScene'],
},
},
},
},
'app.bsky.follow': {
lexicon: 1,
id: 'app.bsky.follow',
@ -2834,7 +2894,89 @@ export const recordSchemaDict: Record<string, RecordSchema> = {
required: ['subject', 'createdAt'],
properties: {
subject: {
type: 'object',
required: ['did', 'declarationCid'],
properties: {
did: {
type: 'string',
},
declarationCid: {
type: 'string',
},
},
},
createdAt: {
type: 'string',
format: 'date-time',
},
},
$defs: {},
},
},
'app.bsky.invite': {
lexicon: 1,
id: 'app.bsky.invite',
type: 'record',
key: 'tid',
record: {
type: 'object',
required: ['group', 'subject', 'createdAt'],
properties: {
group: {
type: 'string',
},
subject: {
type: 'object',
required: ['did', 'declarationCid'],
properties: {
did: {
type: 'string',
},
declarationCid: {
type: 'string',
},
},
},
createdAt: {
type: 'string',
format: 'date-time',
},
},
$defs: {},
},
},
'app.bsky.inviteAccept': {
lexicon: 1,
id: 'app.bsky.inviteAccept',
type: 'record',
key: 'tid',
record: {
type: 'object',
required: ['group', 'invite', 'createdAt'],
properties: {
group: {
type: 'object',
required: ['did', 'declarationCid'],
properties: {
did: {
type: 'string',
},
declarationCid: {
type: 'string',
},
},
},
invite: {
type: 'object',
required: ['uri', 'cid'],
properties: {
uri: {
type: 'string',
},
cid: {
type: 'string',
},
},
},
createdAt: {
type: 'string',

@ -0,0 +1,10 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
export type ActorKnown = 'app.bsky.actorUser' | 'app.bsky.actorScene'
export type ActorUnknown = string
export interface Record {
actorType: ActorKnown | ActorUnknown;
[k: string]: unknown;
}

@ -2,7 +2,11 @@
* GENERATED CODE - DO NOT MODIFY
*/
export interface Record {
subject: string;
subject: {
did: string,
declarationCid: string,
[k: string]: unknown,
};
createdAt: string;
[k: string]: unknown;
}

@ -0,0 +1,13 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
export interface Record {
group: string;
subject: {
did: string,
declarationCid: string,
[k: string]: unknown,
};
createdAt: string;
[k: string]: unknown;
}

@ -0,0 +1,17 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
export interface Record {
group: {
did: string,
declarationCid: string,
[k: string]: unknown,
};
invite: {
uri: string,
cid: string,
[k: string]: unknown,
};
createdAt: string;
[k: string]: unknown;
}

@ -40,6 +40,7 @@ export interface OutputSchema {
refreshJwt: string;
username: string;
did: string;
declarationCid: string;
}
export type Handler = (

@ -5,94 +5,83 @@ export default async (sc: SeedClient) => {
await sc.createAccount('bob', users.bob)
await sc.createAccount('carol', users.carol)
await sc.createAccount('dan', users.dan)
const alice = sc.dids.alice
const bob = sc.dids.bob
const carol = sc.dids.carol
const dan = sc.dids.dan
await sc.createProfile(
sc.dids.alice,
alice,
users.alice.displayName,
users.alice.description,
)
await sc.createProfile(
sc.dids.bob,
users.bob.displayName,
users.bob.description,
)
await sc.follow(sc.dids.alice, sc.dids.bob)
await sc.follow(sc.dids.alice, sc.dids.carol)
await sc.follow(sc.dids.alice, sc.dids.dan)
await sc.follow(sc.dids.carol, sc.dids.alice)
await sc.follow(sc.dids.bob, sc.dids.alice)
await sc.follow(sc.dids.bob, sc.dids.carol)
await sc.follow(sc.dids.dan, sc.dids.bob)
await sc.post(sc.dids.alice, posts.alice[0])
await sc.post(sc.dids.bob, posts.bob[0])
await sc.post(sc.dids.carol, posts.carol[0])
await sc.post(sc.dids.dan, posts.dan[0])
await sc.post(sc.dids.dan, posts.dan[1], [
await sc.createProfile(bob, users.bob.displayName, users.bob.description)
await sc.follow(alice, sc.userRef(bob))
await sc.follow(alice, sc.userRef(carol))
await sc.follow(alice, sc.userRef(dan))
await sc.follow(carol, sc.userRef(alice))
await sc.follow(bob, sc.userRef(alice))
await sc.follow(bob, sc.userRef(carol))
await sc.follow(dan, sc.userRef(bob))
await sc.post(alice, posts.alice[0])
await sc.post(bob, posts.bob[0])
await sc.post(carol, posts.carol[0])
await sc.post(dan, posts.dan[0])
await sc.post(dan, posts.dan[1], [
{
index: [0, 18],
type: 'mention',
value: sc.dids.alice,
value: alice,
},
])
await sc.post(sc.dids.alice, posts.alice[1])
await sc.post(sc.dids.bob, posts.bob[1])
await sc.post(sc.dids.alice, posts.alice[2])
await sc.like(sc.dids.bob, sc.posts[sc.dids.alice][1].ref)
await sc.like(sc.dids.bob, sc.posts[sc.dids.alice][2].ref)
await sc.like(sc.dids.carol, sc.posts[sc.dids.alice][1].ref)
await sc.like(sc.dids.carol, sc.posts[sc.dids.alice][2].ref)
await sc.like(sc.dids.dan, sc.posts[sc.dids.alice][1].ref)
await sc.post(alice, posts.alice[1])
await sc.post(bob, posts.bob[1])
await sc.post(alice, posts.alice[2])
await sc.like(bob, sc.posts[alice][1].ref)
await sc.like(bob, sc.posts[alice][2].ref)
await sc.like(carol, sc.posts[alice][1].ref)
await sc.like(carol, sc.posts[alice][2].ref)
await sc.like(dan, sc.posts[alice][1].ref)
await sc.reply(
sc.dids.bob,
sc.posts[sc.dids.alice][1].ref,
sc.posts[sc.dids.alice][1].ref,
bob,
sc.posts[alice][1].ref,
sc.posts[alice][1].ref,
replies.bob[0],
)
await sc.reply(
sc.dids.carol,
sc.posts[sc.dids.alice][1].ref,
sc.posts[sc.dids.alice][1].ref,
carol,
sc.posts[alice][1].ref,
sc.posts[alice][1].ref,
replies.carol[0],
)
await sc.reply(
sc.dids.alice,
sc.posts[sc.dids.alice][1].ref,
sc.replies[sc.dids.bob][0].ref,
alice,
sc.posts[alice][1].ref,
sc.replies[bob][0].ref,
replies.alice[0],
)
await sc.repost(sc.dids.carol, sc.posts[sc.dids.dan][1].ref)
await sc.repost(sc.dids.dan, sc.posts[sc.dids.alice][1].ref)
await sc.repost(carol, sc.posts[dan][1].ref)
await sc.repost(dan, sc.posts[alice][1].ref)
await sc.createBadge(sc.dids.bob, 'employee')
await sc.createBadge(sc.dids.bob, 'tag', 'cool')
await sc.createBadge(sc.dids.carol, 'tag', 'neat')
await sc.createBadge(sc.dids.carol, 'tag', 'cringe')
await sc.createBadge(bob, 'employee')
await sc.createBadge(bob, 'tag', 'cool')
await sc.createBadge(carol, 'tag', 'neat')
await sc.createBadge(carol, 'tag', 'cringe')
await sc.offerBadge(sc.dids.bob, sc.dids.alice, sc.badges[sc.dids.bob][0])
await sc.offerBadge(sc.dids.bob, sc.dids.alice, sc.badges[sc.dids.bob][1])
await sc.offerBadge(sc.dids.bob, sc.dids.bob, sc.badges[sc.dids.bob][1])
await sc.offerBadge(sc.dids.bob, sc.dids.carol, sc.badges[sc.dids.bob][1])
await sc.offerBadge(sc.dids.bob, sc.dids.dan, sc.badges[sc.dids.bob][1])
await sc.offerBadge(sc.dids.carol, sc.dids.alice, sc.badges[sc.dids.carol][0])
await sc.offerBadge(bob, alice, sc.badges[bob][0])
await sc.offerBadge(bob, alice, sc.badges[bob][1])
await sc.offerBadge(bob, bob, sc.badges[bob][1])
await sc.offerBadge(bob, carol, sc.badges[bob][1])
await sc.offerBadge(bob, dan, sc.badges[bob][1])
await sc.offerBadge(carol, alice, sc.badges[carol][0])
await sc.acceptBadge(alice, sc.badges[bob][1], sc.badgeOffers[bob][alice][1])
await sc.acceptBadge(bob, sc.badges[bob][1], sc.badgeOffers[bob][bob][0])
await sc.acceptBadge(carol, sc.badges[bob][1], sc.badgeOffers[bob][carol][0])
await sc.acceptBadge(
sc.dids.alice,
sc.badges[sc.dids.bob][1],
sc.badgeOffers[sc.dids.bob][sc.dids.alice][1],
)
await sc.acceptBadge(
sc.dids.bob,
sc.badges[sc.dids.bob][1],
sc.badgeOffers[sc.dids.bob][sc.dids.bob][0],
)
await sc.acceptBadge(
sc.dids.carol,
sc.badges[sc.dids.bob][1],
sc.badgeOffers[sc.dids.bob][sc.dids.carol][0],
)
await sc.acceptBadge(
sc.dids.alice,
sc.badges[sc.dids.carol][0],
sc.badgeOffers[sc.dids.carol][sc.dids.alice][0],
alice,
sc.badges[carol][0],
sc.badgeOffers[carol][alice][0],
)
return sc

@ -5,7 +5,7 @@ import { CID } from 'multiformats/cid'
// Makes it simple to create data via the XRPC client,
// and keeps track of all created data in memory for convenience.
class Reference {
class RecordRef {
uri: AtUri
cid: CID
@ -30,6 +30,27 @@ class Reference {
}
}
class UserRef {
did: string
declarationCid: CID
constructor(did: string, declarationCid: CID | string) {
this.did = did
this.declarationCid = CID.parse(declarationCid.toString())
}
get raw(): { did: string; declarationCid: string } {
return {
did: this.did.toString(),
declarationCid: this.declarationCid.toString(),
}
}
get declarationStr(): string {
return this.declarationCid.toString()
}
}
export class SeedClient {
accounts: Record<
string,
@ -40,6 +61,7 @@ export class SeedClient {
username: string
email: string
password: string
ref: UserRef
}
>
profiles: Record<
@ -47,16 +69,16 @@ export class SeedClient {
{
displayName: string
description: string
ref: Reference
ref: RecordRef
}
>
follows: Record<string, Record<string, Reference>>
posts: Record<string, { text: string; ref: Reference }[]>
follows: Record<string, Record<string, RecordRef>>
posts: Record<string, { text: string; ref: RecordRef }[]>
likes: Record<string, Record<string, AtUri>>
replies: Record<string, { text: string; ref: Reference }[]>
reposts: Record<string, Reference[]>
badges: Record<string, Reference[]>
badgeOffers: Record<string, Record<string, Reference[]>>
replies: Record<string, { text: string; ref: RecordRef }[]>
reposts: Record<string, RecordRef[]>
badges: Record<string, RecordRef[]>
badgeOffers: Record<string, Record<string, RecordRef[]>>
dids: Record<string, string>
constructor(public client: ServiceClient) {
@ -86,6 +108,7 @@ export class SeedClient {
...data,
email: params.email,
password: params.password,
ref: new UserRef(data.did, data.declarationCid),
}
return this.accounts[shortName]
}
@ -99,20 +122,23 @@ export class SeedClient {
this.profiles[by] = {
displayName,
description,
ref: new Reference(res.uri, res.cid),
ref: new RecordRef(res.uri, res.cid),
}
return this.profiles[by]
}
async follow(from: string, to: string) {
async follow(from: string, to: UserRef) {
const res = await this.client.app.bsky.follow.create(
{ did: from },
{ subject: to, createdAt: new Date().toISOString() },
{
subject: to.raw,
createdAt: new Date().toISOString(),
},
this.getHeaders(from),
)
this.follows[from] ??= {}
this.follows[from][to] = new Reference(res.uri, res.cid)
return this.follows[from][to]
this.follows[from][to.did] = new RecordRef(res.uri, res.cid)
return this.follows[from][to.did]
}
async post(by: string, text: string, entities?: any) {
@ -124,13 +150,13 @@ export class SeedClient {
this.posts[by] ??= []
const post = {
text,
ref: new Reference(res.uri, res.cid),
ref: new RecordRef(res.uri, res.cid),
}
this.posts[by].push(post)
return post
}
async like(by: string, subject: Reference) {
async like(by: string, subject: RecordRef) {
const res = await this.client.app.bsky.like.create(
{ did: by },
{ subject: subject.raw, createdAt: new Date().toISOString() },
@ -141,7 +167,7 @@ export class SeedClient {
return this.likes[by][subject.uriStr]
}
async reply(by: string, root: Reference, parent: Reference, text: string) {
async reply(by: string, root: RecordRef, parent: RecordRef, text: string) {
const res = await this.client.app.bsky.post.create(
{ did: by },
{
@ -157,20 +183,20 @@ export class SeedClient {
this.replies[by] ??= []
const reply = {
text,
ref: new Reference(res.uri, res.cid),
ref: new RecordRef(res.uri, res.cid),
}
this.replies[by].push(reply)
return reply
}
async repost(by: string, subject: Reference) {
async repost(by: string, subject: RecordRef) {
const res = await this.client.app.bsky.repost.create(
{ did: by },
{ subject: subject.raw, createdAt: new Date().toISOString() },
this.getHeaders(by),
)
this.reposts[by] ??= []
const repost = new Reference(res.uri, res.cid)
const repost = new RecordRef(res.uri, res.cid)
this.reposts[by].push(repost)
return repost
}
@ -188,12 +214,12 @@ export class SeedClient {
this.getHeaders(by),
)
this.badges[by] ??= []
const badge = new Reference(res.uri, res.cid)
const badge = new RecordRef(res.uri, res.cid)
this.badges[by].push(badge)
return badge
}
async offerBadge(from: string, to: string, badge: Reference) {
async offerBadge(from: string, to: string, badge: RecordRef) {
const res = await this.client.app.bsky.badgeOffer.create(
{ did: from },
{
@ -205,12 +231,12 @@ export class SeedClient {
)
this.badgeOffers[from] ??= {}
this.badgeOffers[from][to] ??= []
const offer = new Reference(res.uri, res.cid)
const offer = new RecordRef(res.uri, res.cid)
this.badgeOffers[from][to].push(offer)
return offer
}
async acceptBadge(by: string, badge: Reference, offer: Reference) {
async acceptBadge(by: string, badge: RecordRef, offer: RecordRef) {
await this.client.app.bsky.badgeAccept.create(
{ did: by },
{
@ -222,6 +248,10 @@ export class SeedClient {
)
}
userRef(did: string): UserRef {
return this.accounts[did].ref
}
getHeaders(did: string) {
return SeedClient.getHeaders(this.accounts[did].accessJwt)
}

@ -6,18 +6,23 @@ export default async (sc: SeedClient) => {
await sc.createAccount('carol', users.carol)
await sc.createAccount('dan', users.dan)
await sc.createAccount('eve', users.eve)
await sc.follow(sc.dids.alice, sc.dids.bob)
await sc.follow(sc.dids.alice, sc.dids.carol)
await sc.follow(sc.dids.alice, sc.dids.dan)
await sc.follow(sc.dids.alice, sc.dids.eve)
await sc.follow(sc.dids.carol, sc.dids.alice)
await sc.follow(sc.dids.bob, sc.dids.alice)
await sc.follow(sc.dids.bob, sc.dids.carol)
await sc.follow(sc.dids.dan, sc.dids.alice)
await sc.follow(sc.dids.dan, sc.dids.bob)
await sc.follow(sc.dids.dan, sc.dids.eve)
await sc.follow(sc.dids.eve, sc.dids.alice)
await sc.follow(sc.dids.eve, sc.dids.carol)
const alice = sc.dids.alice
const bob = sc.dids.bob
const carol = sc.dids.carol
const dan = sc.dids.dan
const eve = sc.dids.eve
await sc.follow(alice, sc.userRef(bob))
await sc.follow(alice, sc.userRef(carol))
await sc.follow(alice, sc.userRef(dan))
await sc.follow(alice, sc.userRef(eve))
await sc.follow(carol, sc.userRef(alice))
await sc.follow(bob, sc.userRef(alice))
await sc.follow(bob, sc.userRef(carol))
await sc.follow(dan, sc.userRef(alice))
await sc.follow(dan, sc.userRef(bob))
await sc.follow(dan, sc.userRef(eve))
await sc.follow(eve, sc.userRef(alice))
await sc.follow(eve, sc.userRef(carol))
}
const users = {

@ -283,7 +283,10 @@ Array [
"record": Object {
"$type": "app.bsky.follow",
"createdAt": "1970-01-01T00:00:00.000Z",
"subject": "user(1)",
"subject": Object {
"declarationCid": "cids(18)",
"did": "user(1)",
},
},
"uri": "record(17)",
},
@ -292,14 +295,17 @@ Array [
"did": "user(0)",
"name": "carol.test",
},
"cid": "cids(18)",
"cid": "cids(19)",
"indexedAt": "1970-01-01T00:00:00.000Z",
"isRead": true,
"reason": "follow",
"record": Object {
"$type": "app.bsky.follow",
"createdAt": "1970-01-01T00:00:00.000Z",
"subject": "user(1)",
"subject": Object {
"declarationCid": "cids(18)",
"did": "user(1)",
},
},
"uri": "record(18)",
},
@ -589,7 +595,10 @@ Array [
"record": Object {
"$type": "app.bsky.follow",
"createdAt": "1970-01-01T00:00:00.000Z",
"subject": "user(1)",
"subject": Object {
"declarationCid": "cids(18)",
"did": "user(1)",
},
},
"uri": "record(17)",
},
@ -598,14 +607,17 @@ Array [
"did": "user(0)",
"name": "carol.test",
},
"cid": "cids(18)",
"cid": "cids(19)",
"indexedAt": "1970-01-01T00:00:00.000Z",
"isRead": false,
"reason": "follow",
"record": Object {
"$type": "app.bsky.follow",
"createdAt": "1970-01-01T00:00:00.000Z",
"subject": "user(1)",
"subject": Object {
"declarationCid": "cids(18)",
"did": "user(1)",
},
},
"uri": "record(18)",
},