Admin labeling ()

* adding to moderation flows

* some bugfixing in labels

* hardcode profile nsid

* make labels off moderation action

* db updates

* wip

* report labels in admin views & reverse moderation actions

* Test admin get record and repo w/ labels

* update db

* fix

* exclude negs from labels

* exclude neg on moderation views as well

* Check-in missing lex

* Check-in missing lex

* In-progress admin label tests

* Test label creation/reversal via actions

* Admin label test snapshots ()

* new snapshots for label on user

* fix get moderation action snap

* fix dev-env

---------

Co-authored-by: Devin Ivy <devinivy@gmail.com>
This commit is contained in:
Daniel Holmgren 2023-04-12 22:53:32 -05:00 committed by GitHub
parent 6592fcd6eb
commit aa46ad1e1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1882 additions and 424 deletions

@ -146,6 +146,10 @@
"relatedRecords": {"type": "array", "items": {"type": "unknown"}},
"indexedAt": {"type": "string", "format": "datetime"},
"moderation": {"type": "ref", "ref": "#moderationDetail"},
"labels": {
"type": "array",
"items": {"type": "ref", "ref": "com.atproto.label.defs#label"}
},
"invitedBy": {"type": "ref", "ref": "com.atproto.server.defs#inviteCode"},
"invites": {
"type": "array",
@ -181,6 +185,10 @@
"cid": {"type": "string", "format": "cid"},
"value": {"type": "unknown"},
"blobs": {"type": "array", "items": {"type": "ref", "ref": "#blobView"}},
"labels": {
"type": "array",
"items": {"type": "ref", "ref": "com.atproto.label.defs#label"}
},
"indexedAt": {"type": "string", "format": "datetime"},
"moderation": {"type": "ref", "ref": "#moderationDetail"},
"repo": {"type": "ref", "ref": "#repoView"}

@ -364,6 +364,13 @@ export const schemaDict = {
type: 'ref',
ref: 'lex:com.atproto.admin.defs#moderationDetail',
},
labels: {
type: 'array',
items: {
type: 'ref',
ref: 'lex:com.atproto.label.defs#label',
},
},
invitedBy: {
type: 'ref',
ref: 'lex:com.atproto.server.defs#inviteCode',
@ -461,6 +468,13 @@ export const schemaDict = {
ref: 'lex:com.atproto.admin.defs#blobView',
},
},
labels: {
type: 'array',
items: {
type: 'ref',
ref: 'lex:com.atproto.label.defs#label',
},
},
indexedAt: {
type: 'string',
format: 'datetime',
@ -1055,7 +1069,7 @@ export const schemaDict = {
defs: {
main: {
type: 'procedure',
description: 'Administrative action to update an accounts handle',
description: "Administrative action to update an account's handle",
input: {
encoding: 'application/json',
schema: {

@ -8,6 +8,7 @@ import { CID } from 'multiformats/cid'
import * as ComAtprotoRepoStrongRef from '../repo/strongRef'
import * as ComAtprotoModerationDefs from '../moderation/defs'
import * as ComAtprotoServerDefs from '../server/defs'
import * as ComAtprotoLabelDefs from '../label/defs'
export interface ActionView {
id: number
@ -195,6 +196,7 @@ export interface RepoViewDetail {
relatedRecords: {}[]
indexedAt: string
moderation: ModerationDetail
labels?: ComAtprotoLabelDefs.Label[]
invitedBy?: ComAtprotoServerDefs.InviteCode
invites?: ComAtprotoServerDefs.InviteCode[]
[k: string]: unknown
@ -257,6 +259,7 @@ export interface RecordViewDetail {
cid: string
value: {}
blobs: BlobView[]
labels?: ComAtprotoLabelDefs.Label[]
indexedAt: string
moderation: ModerationDetail
repo: RepoView

@ -21,6 +21,7 @@ import * as ComAtprotoAdminResolveModerationReports from './types/com/atproto/ad
import * as ComAtprotoAdminReverseModerationAction from './types/com/atproto/admin/reverseModerationAction'
import * as ComAtprotoAdminSearchRepos from './types/com/atproto/admin/searchRepos'
import * as ComAtprotoAdminTakeModerationAction from './types/com/atproto/admin/takeModerationAction'
import * as ComAtprotoAdminUpdateAccountHandle from './types/com/atproto/admin/updateAccountHandle'
import * as ComAtprotoIdentityResolveHandle from './types/com/atproto/identity/resolveHandle'
import * as ComAtprotoIdentityUpdateHandle from './types/com/atproto/identity/updateHandle'
import * as ComAtprotoLabelQueryLabels from './types/com/atproto/label/queryLabels'
@ -36,6 +37,7 @@ import * as ComAtprotoRepoPutRecord from './types/com/atproto/repo/putRecord'
import * as ComAtprotoRepoUploadBlob from './types/com/atproto/repo/uploadBlob'
import * as ComAtprotoServerCreateAccount from './types/com/atproto/server/createAccount'
import * as ComAtprotoServerCreateInviteCode from './types/com/atproto/server/createInviteCode'
import * as ComAtprotoServerCreateInviteCodes from './types/com/atproto/server/createInviteCodes'
import * as ComAtprotoServerCreateSession from './types/com/atproto/server/createSession'
import * as ComAtprotoServerDeleteAccount from './types/com/atproto/server/deleteAccount'
import * as ComAtprotoServerDeleteSession from './types/com/atproto/server/deleteSession'
@ -54,6 +56,7 @@ import * as ComAtprotoSyncGetHead from './types/com/atproto/sync/getHead'
import * as ComAtprotoSyncGetRecord from './types/com/atproto/sync/getRecord'
import * as ComAtprotoSyncGetRepo from './types/com/atproto/sync/getRepo'
import * as ComAtprotoSyncListBlobs from './types/com/atproto/sync/listBlobs'
import * as ComAtprotoSyncListRepos from './types/com/atproto/sync/listRepos'
import * as ComAtprotoSyncNotifyOfUpdate from './types/com/atproto/sync/notifyOfUpdate'
import * as ComAtprotoSyncRequestCrawl from './types/com/atproto/sync/requestCrawl'
import * as ComAtprotoSyncSubscribeRepos from './types/com/atproto/sync/subscribeRepos'
@ -253,6 +256,16 @@ export class AdminNS {
const nsid = 'com.atproto.admin.takeModerationAction' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
updateAccountHandle<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,
ComAtprotoAdminUpdateAccountHandle.Handler<ExtractAuth<AV>>
>,
) {
const nsid = 'com.atproto.admin.updateAccountHandle' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
}
export class IdentityNS {
@ -405,6 +418,16 @@ export class ServerNS {
return this._server.xrpc.method(nsid, cfg)
}
createInviteCodes<AV extends AuthVerifier>(
cfg: ConfigOf<
AV,
ComAtprotoServerCreateInviteCodes.Handler<ExtractAuth<AV>>
>,
) {
const nsid = 'com.atproto.server.createInviteCodes' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
createSession<AV extends AuthVerifier>(
cfg: ConfigOf<AV, ComAtprotoServerCreateSession.Handler<ExtractAuth<AV>>>,
) {
@ -548,6 +571,13 @@ export class SyncNS {
return this._server.xrpc.method(nsid, cfg)
}
listRepos<AV extends AuthVerifier>(
cfg: ConfigOf<AV, ComAtprotoSyncListRepos.Handler<ExtractAuth<AV>>>,
) {
const nsid = 'com.atproto.sync.listRepos' // @ts-ignore
return this._server.xrpc.method(nsid, cfg)
}
notifyOfUpdate<AV extends AuthVerifier>(
cfg: ConfigOf<AV, ComAtprotoSyncNotifyOfUpdate.Handler<ExtractAuth<AV>>>,
) {

@ -364,6 +364,13 @@ export const schemaDict = {
type: 'ref',
ref: 'lex:com.atproto.admin.defs#moderationDetail',
},
labels: {
type: 'array',
items: {
type: 'ref',
ref: 'lex:com.atproto.label.defs#label',
},
},
invitedBy: {
type: 'ref',
ref: 'lex:com.atproto.server.defs#inviteCode',
@ -461,6 +468,13 @@ export const schemaDict = {
ref: 'lex:com.atproto.admin.defs#blobView',
},
},
labels: {
type: 'array',
items: {
type: 'ref',
ref: 'lex:com.atproto.label.defs#label',
},
},
indexedAt: {
type: 'string',
format: 'datetime',
@ -1049,6 +1063,33 @@ export const schemaDict = {
},
},
},
ComAtprotoAdminUpdateAccountHandle: {
lexicon: 1,
id: 'com.atproto.admin.updateAccountHandle',
defs: {
main: {
type: 'procedure',
description: "Administrative action to update an account's handle",
input: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['did', 'handle'],
properties: {
did: {
type: 'string',
format: 'did',
},
handle: {
type: 'string',
format: 'handle',
},
},
},
},
},
},
},
ComAtprotoIdentityResolveHandle: {
lexicon: 1,
id: 'com.atproto.identity.resolveHandle',
@ -1113,44 +1154,40 @@ export const schemaDict = {
label: {
type: 'object',
description: 'Metadata tag on an atproto resource (eg, repo or record)',
key: 'tid',
record: {
type: 'object',
required: ['src', 'uri', 'val', 'cts'],
properties: {
src: {
type: 'string',
format: 'did',
description: 'DID of the actor who created this label',
},
uri: {
type: 'string',
format: 'uri',
description:
'AT URI of the record, repository (account), or other resource which this label applies to',
},
cid: {
type: 'string',
format: 'cid',
description:
"optionally, CID specifying the specific version of 'uri' resource this label applies to",
},
val: {
type: 'string',
maxLength: 128,
description:
'the short string name of the value or type of this label',
},
neg: {
type: 'boolean',
description:
'if true, this is a negation label, overwriting a previous label',
},
cts: {
type: 'string',
format: 'datetime',
description: 'timestamp when this label was created',
},
required: ['src', 'uri', 'val', 'cts'],
properties: {
src: {
type: 'string',
format: 'did',
description: 'DID of the actor who created this label',
},
uri: {
type: 'string',
format: 'uri',
description:
'AT URI of the record, repository (account), or other resource which this label applies to',
},
cid: {
type: 'string',
format: 'cid',
description:
"optionally, CID specifying the specific version of 'uri' resource this label applies to",
},
val: {
type: 'string',
maxLength: 128,
description:
'the short string name of the value or type of this label',
},
neg: {
type: 'boolean',
description:
'if true, this is a negation label, overwriting a previous label',
},
cts: {
type: 'string',
format: 'datetime',
description: 'timestamp when this label was created',
},
},
},
@ -2049,6 +2086,51 @@ export const schemaDict = {
},
},
},
ComAtprotoServerCreateInviteCodes: {
lexicon: 1,
id: 'com.atproto.server.createInviteCodes',
defs: {
main: {
type: 'procedure',
description: 'Create an invite code.',
input: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['codeCount', 'useCount'],
properties: {
codeCount: {
type: 'integer',
default: 1,
},
useCount: {
type: 'integer',
},
forAccount: {
type: 'string',
format: 'did',
},
},
},
},
output: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['codes'],
properties: {
codes: {
type: 'array',
items: {
type: 'string',
},
},
},
},
},
},
},
},
ComAtprotoServerCreateSession: {
lexicon: 1,
id: 'com.atproto.server.createSession',
@ -2060,7 +2142,7 @@ export const schemaDict = {
encoding: 'application/json',
schema: {
type: 'object',
required: ['password'],
required: ['identifier', 'password'],
properties: {
identifier: {
type: 'string',
@ -2726,6 +2808,63 @@ export const schemaDict = {
},
},
},
ComAtprotoSyncListRepos: {
lexicon: 1,
id: 'com.atproto.sync.listRepos',
defs: {
main: {
type: 'query',
description: 'List dids and root cids of hosted repos',
parameters: {
type: 'params',
properties: {
limit: {
type: 'integer',
minimum: 1,
maximum: 1000,
default: 500,
},
cursor: {
type: 'string',
},
},
},
output: {
encoding: 'application/json',
schema: {
type: 'object',
required: ['repos'],
properties: {
cursor: {
type: 'string',
},
repos: {
type: 'array',
items: {
type: 'ref',
ref: 'lex:com.atproto.sync.listRepos#repo',
},
},
},
},
},
},
repo: {
type: 'object',
required: ['did', 'head'],
properties: {
did: {
type: 'string',
format: 'did',
},
head: {
type: 'string',
format: 'cid',
},
},
},
},
},
ComAtprotoSyncNotifyOfUpdate: {
lexicon: 1,
id: 'com.atproto.sync.notifyOfUpdate',
@ -4657,6 +4796,7 @@ export const ids = {
'com.atproto.admin.reverseModerationAction',
ComAtprotoAdminSearchRepos: 'com.atproto.admin.searchRepos',
ComAtprotoAdminTakeModerationAction: 'com.atproto.admin.takeModerationAction',
ComAtprotoAdminUpdateAccountHandle: 'com.atproto.admin.updateAccountHandle',
ComAtprotoIdentityResolveHandle: 'com.atproto.identity.resolveHandle',
ComAtprotoIdentityUpdateHandle: 'com.atproto.identity.updateHandle',
ComAtprotoLabelDefs: 'com.atproto.label.defs',
@ -4675,6 +4815,7 @@ export const ids = {
ComAtprotoRepoUploadBlob: 'com.atproto.repo.uploadBlob',
ComAtprotoServerCreateAccount: 'com.atproto.server.createAccount',
ComAtprotoServerCreateInviteCode: 'com.atproto.server.createInviteCode',
ComAtprotoServerCreateInviteCodes: 'com.atproto.server.createInviteCodes',
ComAtprotoServerCreateSession: 'com.atproto.server.createSession',
ComAtprotoServerDefs: 'com.atproto.server.defs',
ComAtprotoServerDeleteAccount: 'com.atproto.server.deleteAccount',
@ -4697,6 +4838,7 @@ export const ids = {
ComAtprotoSyncGetRecord: 'com.atproto.sync.getRecord',
ComAtprotoSyncGetRepo: 'com.atproto.sync.getRepo',
ComAtprotoSyncListBlobs: 'com.atproto.sync.listBlobs',
ComAtprotoSyncListRepos: 'com.atproto.sync.listRepos',
ComAtprotoSyncNotifyOfUpdate: 'com.atproto.sync.notifyOfUpdate',
ComAtprotoSyncRequestCrawl: 'com.atproto.sync.requestCrawl',
ComAtprotoSyncSubscribeRepos: 'com.atproto.sync.subscribeRepos',

@ -8,6 +8,7 @@ import { CID } from 'multiformats/cid'
import * as ComAtprotoRepoStrongRef from '../repo/strongRef'
import * as ComAtprotoModerationDefs from '../moderation/defs'
import * as ComAtprotoServerDefs from '../server/defs'
import * as ComAtprotoLabelDefs from '../label/defs'
export interface ActionView {
id: number
@ -195,6 +196,7 @@ export interface RepoViewDetail {
relatedRecords: {}[]
indexedAt: string
moderation: ModerationDetail
labels?: ComAtprotoLabelDefs.Label[]
invitedBy?: ComAtprotoServerDefs.InviteCode
invites?: ComAtprotoServerDefs.InviteCode[]
[k: string]: unknown
@ -257,6 +259,7 @@ export interface RecordViewDetail {
cid: string
value: {}
blobs: BlobView[]
labels?: ComAtprotoLabelDefs.Label[]
indexedAt: string
moderation: ModerationDetail
repo: RepoView

@ -0,0 +1,36 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import express from 'express'
import { ValidationResult, BlobRef } from '@atproto/lexicon'
import { lexicons } from '../../../../lexicons'
import { isObj, hasProp } from '../../../../util'
import { CID } from 'multiformats/cid'
import { HandlerAuth } from '@atproto/xrpc-server'
export interface QueryParams {}
export interface InputSchema {
did: string
handle: string
[k: string]: unknown
}
export interface HandlerInput {
encoding: 'application/json'
body: InputSchema
}
export interface HandlerError {
status: number
message?: string
}
export type HandlerOutput = HandlerError | void
export type Handler<HA extends HandlerAuth = never> = (ctx: {
auth: HA
params: QueryParams
input: HandlerInput
req: express.Request
res: express.Response
}) => Promise<HandlerOutput> | HandlerOutput

@ -7,7 +7,21 @@ import { isObj, hasProp } from '../../../../util'
import { CID } from 'multiformats/cid'
/** Metadata tag on an atproto resource (eg, repo or record) */
export interface Label {}
export interface Label {
/** DID of the actor who created this label */
src: string
/** AT URI of the record, repository (account), or other resource which this label applies to */
uri: string
/** optionally, CID specifying the specific version of 'uri' resource this label applies to */
cid?: string
/** the short string name of the value or type of this label */
val: string
/** if true, this is a negation label, overwriting a previous label */
neg?: boolean
/** timestamp when this label was created */
cts: string
[k: string]: unknown
}
export function isLabel(v: unknown): v is Label {
return (

@ -0,0 +1,47 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import express from 'express'
import { ValidationResult, BlobRef } from '@atproto/lexicon'
import { lexicons } from '../../../../lexicons'
import { isObj, hasProp } from '../../../../util'
import { CID } from 'multiformats/cid'
import { HandlerAuth } from '@atproto/xrpc-server'
export interface QueryParams {}
export interface InputSchema {
codeCount: number
useCount: number
forAccount?: string
[k: string]: unknown
}
export interface OutputSchema {
codes: string[]
[k: string]: unknown
}
export interface HandlerInput {
encoding: 'application/json'
body: InputSchema
}
export interface HandlerSuccess {
encoding: 'application/json'
body: OutputSchema
}
export interface HandlerError {
status: number
message?: string
}
export type HandlerOutput = HandlerError | HandlerSuccess
export type Handler<HA extends HandlerAuth = never> = (ctx: {
auth: HA
params: QueryParams
input: HandlerInput
req: express.Request
res: express.Response
}) => Promise<HandlerOutput> | HandlerOutput

@ -12,7 +12,7 @@ export interface QueryParams {}
export interface InputSchema {
/** Handle or other identifier supported by the server for the authenticating user. */
identifier?: string
identifier: string
password: string
[k: string]: unknown
}

@ -0,0 +1,61 @@
/**
* GENERATED CODE - DO NOT MODIFY
*/
import express from 'express'
import { ValidationResult, BlobRef } from '@atproto/lexicon'
import { lexicons } from '../../../../lexicons'
import { isObj, hasProp } from '../../../../util'
import { CID } from 'multiformats/cid'
import { HandlerAuth } from '@atproto/xrpc-server'
export interface QueryParams {
limit: number
cursor?: string
}
export type InputSchema = undefined
export interface OutputSchema {
cursor?: string
repos: Repo[]
[k: string]: unknown
}
export type HandlerInput = undefined
export interface HandlerSuccess {
encoding: 'application/json'
body: OutputSchema
}
export interface HandlerError {
status: number
message?: string
}
export type HandlerOutput = HandlerError | HandlerSuccess
export type Handler<HA extends HandlerAuth = never> = (ctx: {
auth: HA
params: QueryParams
input: HandlerInput
req: express.Request
res: express.Response
}) => Promise<HandlerOutput> | HandlerOutput
export interface Repo {
did: string
head: string
[k: string]: unknown
}
export function isRepo(v: unknown): v is Repo {
return (
isObj(v) &&
hasProp(v, '$type') &&
v.$type === 'com.atproto.sync.listRepos#repo'
)
}
export function validateRepo(v: unknown): ValidationResult {
return lexicons.validate('com.atproto.sync.listRepos#repo', v)
}

@ -201,20 +201,20 @@ export async function generateMockSetup(env: DevEnv) {
.insertInto('label')
.values([
{
sourceDid: ctx.cfg.labelerDid,
subjectUri: labeledPost.uri,
subjectCid: labeledPost.cid,
value: 'nudity',
negated: 0,
createdAt: new Date().toISOString(),
src: ctx.cfg.labelerDid,
uri: labeledPost.uri,
cid: labeledPost.cid,
val: 'nudity',
neg: 0,
cts: new Date().toISOString(),
},
{
sourceDid: ctx.cfg.labelerDid,
subjectUri: filteredPost.uri,
subjectCid: filteredPost.cid,
value: 'dmca-violation',
negated: 0,
createdAt: new Date().toISOString(),
src: ctx.cfg.labelerDid,
uri: filteredPost.uri,
cid: filteredPost.cid,
val: 'dmca-violation',
neg: 0,
cts: new Date().toISOString(),
},
])
.execute()

@ -14,6 +14,7 @@ export default function (server: Server, ctx: AppContext) {
const moderationAction = await db.transaction(async (dbTxn) => {
const moderationTxn = services.moderation(dbTxn)
const labelTxn = services.appView.label(dbTxn)
const now = new Date()
const existing = await moderationTxn.getAction(id)
@ -53,6 +54,16 @@ export default function (server: Server, ctx: AppContext) {
})
}
// invert creates & negates
const negate = result.createLabelVals?.split(' ')
const create = result.negateLabelVals?.split(' ')
await labelTxn.formatAndCreate(
ctx.cfg.labelerDid,
result.subjectUri ?? result.subjectDid,
result.subjectCid,
{ create, negate },
)
return result
})

@ -4,6 +4,7 @@ import { Server } from '../../../../lexicon'
import AppContext from '../../../../context'
import { TAKEDOWN } from '../../../../lexicon/types/com/atproto/admin/defs'
import { getSubject, getAction } from '../moderation/util'
import { InvalidRequestError } from '@atproto/xrpc-server'
export default function (server: Server, ctx: AppContext) {
server.com.atproto.admin.takeModerationAction({
@ -11,16 +12,29 @@ export default function (server: Server, ctx: AppContext) {
handler: async ({ input }) => {
const { db, services } = ctx
const moderationService = services.moderation(db)
const { action, subject, reason, createdBy, subjectBlobCids } = input.body
const {
action,
subject,
reason,
createdBy,
createLabelVals,
negateLabelVals,
subjectBlobCids,
} = input.body
validateLabels([...(createLabelVals ?? []), ...(negateLabelVals ?? [])])
const moderationAction = await db.transaction(async (dbTxn) => {
const authTxn = services.auth(dbTxn)
const moderationTxn = services.moderation(dbTxn)
const labelTxn = services.appView.label(dbTxn)
const result = await moderationTxn.logAction({
action: getAction(action),
subject: getSubject(subject),
subjectBlobCids: subjectBlobCids?.map((cid) => CID.parse(cid)) ?? [],
createLabelVals,
negateLabelVals,
createdBy,
reason,
})
@ -49,6 +63,13 @@ export default function (server: Server, ctx: AppContext) {
})
}
await labelTxn.formatAndCreate(
ctx.cfg.labelerDid,
result.subjectUri ?? result.subjectDid,
result.subjectCid,
{ create: createLabelVals, negate: negateLabelVals },
)
return result
})
@ -59,3 +80,15 @@ export default function (server: Server, ctx: AppContext) {
},
})
}
const validateLabels = (labels: string[]) => {
for (const label of labels) {
for (const char of badChars) {
if (label.includes(char)) {
throw new InvalidRequestError(`Invalid label: ${label}`)
}
}
}
}
const badChars = [' ', ',', ';', `'`, `"`]

@ -2,6 +2,7 @@ import { AtUri } from '@atproto/uri'
import Database from '../../../db'
import { Label } from '../../../lexicon/types/com/atproto/label/defs'
import { ids } from '../../../lexicon/lexicons'
import { sql } from 'kysely'
export type Labels = Record<string, Label[]>
@ -12,34 +13,86 @@ export class LabelService {
return (db: Database) => new LabelService(db)
}
async getLabelsForSubjects(subjects: string[]): Promise<Labels> {
async formatAndCreate(
src: string,
uri: string,
cid: string | null,
labels: { create?: string[]; negate?: string[] },
) {
const { create = [], negate = [] } = labels
const toCreate = create.map((val) => ({
src,
uri,
cid: cid ?? undefined,
val,
neg: false,
cts: new Date().toISOString(),
}))
const toNegate = negate.map((val) => ({
src,
uri,
cid: cid ?? undefined,
val,
neg: true,
cts: new Date().toISOString(),
}))
await this.createLabels([...toCreate, ...toNegate])
}
async createLabels(labels: Label[]) {
if (labels.length < 1) return
const dbVals = labels.map((l) => ({
...l,
cid: l.cid ?? '',
neg: (l.neg ? 1 : 0) as 1 | 0,
}))
const { ref } = this.db.db.dynamic
const excluded = (col: string) => ref(`excluded.${col}`)
await this.db.db
.insertInto('label')
.values(dbVals)
.onConflict((oc) =>
oc.columns(['src', 'uri', 'cid', 'val']).doUpdateSet({
neg: sql`${excluded('neg')}`,
cts: sql`${excluded('cts')}`,
}),
)
.execute()
}
async getLabelsForSubjects(
subjects: string[],
includeNeg?: boolean,
): Promise<Labels> {
if (subjects.length < 1) return {}
const res = await this.db.db
.selectFrom('label')
.where('label.subjectUri', 'in', subjects)
.where('label.uri', 'in', subjects)
.if(!includeNeg, (qb) => qb.where('neg', '=', 0))
.selectAll()
.execute()
return res.reduce((acc, cur) => {
acc[cur.subjectUri] ??= []
acc[cur.subjectUri].push({
src: cur.sourceDid,
uri: cur.subjectUri,
cid: cur.subjectCid ?? undefined,
val: cur.value,
neg: cur.negated === 1, // @TODO update in appview
cts: cur.createdAt,
acc[cur.uri] ??= []
acc[cur.uri].push({
...cur,
cid: cur.cid === '' ? undefined : cur.cid,
neg: cur.neg === 1, // @TODO update in appview
})
return acc
}, {} as Labels)
}
async getLabelsForProfiles(dids: string[]): Promise<Labels> {
// gets labels for both did & profile record
async getLabelsForProfiles(
dids: string[],
includeNeg?: boolean,
): Promise<Labels> {
if (dids.length < 1) return {}
const profileUris = dids.map((did) =>
AtUri.make(did, ids.AppBskyActorProfile, 'self').toString(),
)
const subjects = [...dids, ...profileUris]
const labels = await this.getLabelsForSubjects(subjects)
const labels = await this.getLabelsForSubjects(subjects, includeNeg)
// combine labels for profile + did
return Object.keys(labels).reduce((acc, cur) => {
const did = cur.startsWith('at://') ? new AtUri(cur).hostname : cur
@ -49,13 +102,16 @@ export class LabelService {
}, {} as Labels)
}
async getLabels(subject: string): Promise<Label[]> {
const labels = await this.getLabelsForSubjects([subject])
async getLabels(subject: string, includeNeg?: boolean): Promise<Label[]> {
const labels = await this.getLabelsForSubjects([subject], includeNeg)
return labels[subject] ?? []
}
async getLabelsForProfile(did: string): Promise<Label[]> {
const labels = await this.getLabelsForProfiles([did])
async getLabelsForProfile(
did: string,
includeNeg?: boolean,
): Promise<Label[]> {
const labels = await this.getLabelsForProfiles([did], includeNeg)
return labels[did] ?? []
}
}

@ -0,0 +1,70 @@
import { Kysely } from 'kysely'
const moderationActionTable = 'moderation_action'
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.alterTable(moderationActionTable)
.addColumn('createLabelVals', 'varchar')
.execute()
await db.schema
.alterTable(moderationActionTable)
.addColumn('negateLabelVals', 'varchar')
.execute()
await db.schema.dropTable('label').execute()
await db.schema
.createTable('label')
.addColumn('src', 'varchar', (col) => col.notNull())
.addColumn('uri', 'varchar', (col) => col.notNull())
.addColumn('cid', 'varchar', (col) => col.notNull())
.addColumn('val', 'varchar', (col) => col.notNull())
.addColumn('neg', 'int2', (col) => col.notNull()) // @TODO convert to boolean in appview
.addColumn('cts', 'varchar', (col) => col.notNull())
.addPrimaryKeyConstraint('label_pkey', ['src', 'uri', 'cid', 'val'])
.execute()
await db.schema
.createIndex('label_uri_index')
.on('label')
.column('uri')
.execute()
}
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema
.alterTable(moderationActionTable)
.dropColumn('createLabelVals')
.execute()
await db.schema
.alterTable(moderationActionTable)
.dropColumn('negateLabelVals')
.execute()
await db.schema.dropTable('label').execute()
await db.schema
.createTable('label')
.addColumn('sourceDid', 'varchar', (col) => col.notNull())
.addColumn('subjectUri', 'varchar', (col) => col.notNull())
.addColumn('subjectCid', 'varchar')
.addColumn('value', 'varchar', (col) => col.notNull())
.addColumn('negated', 'int2', (col) => col.notNull()) // @TODO convert to boolean in appview
.addColumn('createdAt', 'varchar', (col) => col.notNull())
.addPrimaryKeyConstraint('label_pkey', [
'sourceDid',
'subjectUri',
'subjectCid',
'value',
])
.execute()
await db.schema
.createIndex('label_subject_uri_index')
.on('label')
.column('subjectUri')
.execute()
}

@ -38,3 +38,4 @@ export * as _20230328T214311005Z from './20230328T214311005Z-rework-seq'
export * as _20230406T185855842Z from './20230406T185855842Z-feed-item-init'
export * as _20230411T175730759Z from './20230411T175730759Z-drop-message-queue'
export * as _20230411T180247652Z from './20230411T180247652Z-labels'
export * as _20230412T231807162Z from './20230412T231807162Z-moderation-action-labels'

@ -1,12 +1,12 @@
export const tableName = 'label'
export interface Label {
sourceDid: string
subjectUri: string
subjectCid: string | null
value: string
negated: 0 | 1 // @TODO convert to boolean in app-view
createdAt: string
src: string
uri: string
cid: string
val: string
neg: 0 | 1 // @TODO convert to boolean in app-view
cts: string
}
export type PartialDB = { [tableName]: Label }

@ -21,6 +21,8 @@ export interface ModerationAction {
subjectDid: string
subjectUri: string | null
subjectCid: string | null
createLabelVals: string | null
negateLabelVals: string | null
reason: string
createdAt: string
createdBy: string

@ -37,13 +37,13 @@ export abstract class Labeler {
const labels = await this.labelRecord(obj)
if (labels.length < 1) return
const cid = await cidForRecord(obj)
const rows = labels.map((value) => ({
sourceDid: this.labelerDid,
subjectUri: uri.toString(),
subjectCid: cid.toString(),
value,
negated: 0 as const,
createdAt: new Date().toISOString(),
const rows = labels.map((val) => ({
src: this.labelerDid,
uri: uri.toString(),
cid: cid.toString(),
val,
neg: 0 as const,
cts: new Date().toISOString(),
}))
await this.db.db

@ -364,6 +364,13 @@ export const schemaDict = {
type: 'ref',
ref: 'lex:com.atproto.admin.defs#moderationDetail',
},
labels: {
type: 'array',
items: {
type: 'ref',
ref: 'lex:com.atproto.label.defs#label',
},
},
invitedBy: {
type: 'ref',
ref: 'lex:com.atproto.server.defs#inviteCode',
@ -461,6 +468,13 @@ export const schemaDict = {
ref: 'lex:com.atproto.admin.defs#blobView',
},
},
labels: {
type: 'array',
items: {
type: 'ref',
ref: 'lex:com.atproto.label.defs#label',
},
},
indexedAt: {
type: 'string',
format: 'datetime',
@ -1055,7 +1069,7 @@ export const schemaDict = {
defs: {
main: {
type: 'procedure',
description: 'Administrative action to update an accounts handle',
description: "Administrative action to update an account's handle",
input: {
encoding: 'application/json',
schema: {

@ -8,6 +8,7 @@ import { CID } from 'multiformats/cid'
import * as ComAtprotoRepoStrongRef from '../repo/strongRef'
import * as ComAtprotoModerationDefs from '../moderation/defs'
import * as ComAtprotoServerDefs from '../server/defs'
import * as ComAtprotoLabelDefs from '../label/defs'
export interface ActionView {
id: number
@ -195,6 +196,7 @@ export interface RepoViewDetail {
relatedRecords: {}[]
indexedAt: string
moderation: ModerationDetail
labels?: ComAtprotoLabelDefs.Label[]
invitedBy?: ComAtprotoServerDefs.InviteCode
invites?: ComAtprotoServerDefs.InviteCode[]
[k: string]: unknown
@ -257,6 +259,7 @@ export interface RecordViewDetail {
cid: string
value: {}
blobs: BlobView[]
labels?: ComAtprotoLabelDefs.Label[]
indexedAt: string
moderation: ModerationDetail
repo: RepoView

@ -178,6 +178,8 @@ export class ModerationService {
subject: { did: string } | { uri: AtUri; cid: CID }
subjectBlobCids?: CID[]
reason: string
createLabelVals?: string[]
negateLabelVals?: string[]
createdBy: string
createdAt?: Date
}): Promise<ModerationActionRow> {
@ -190,6 +192,8 @@ export class ModerationService {
subjectBlobCids,
createdAt = new Date(),
} = info
const createLabelVals = info.createLabelVals?.join()
const negateLabelVals = info.negateLabelVals?.join()
// Resolve subject info
let subjectInfo: SubjectInfo
@ -248,6 +252,8 @@ export class ModerationService {
reason,
createdAt: createdAt.toISOString(),
createdBy,
createLabelVals,
negateLabelVals,
...subjectInfo,
})
.returningAll()

@ -17,6 +17,7 @@ import {
BlobView,
} from '../../lexicon/types/com/atproto/admin/defs'
import { OutputSchema as ReportOutput } from '../../lexicon/types/com/atproto/moderation/createReport'
import { Label } from '../../lexicon/types/com/atproto/label/defs'
import { ModerationAction, ModerationReport } from '../../db/tables/moderation'
import { AccountService } from '../account'
import { RecordService } from '../record'
@ -127,9 +128,10 @@ export class ModerationViews {
.execute(),
this.services.account(this.db).getAccountInviteCodes(repo.did),
])
const [reports, actions] = await Promise.all([
const [reports, actions, labels] = await Promise.all([
this.report(reportResults),
this.action(actionResults),
this.labels(repo.did),
])
return {
...repo,
@ -139,6 +141,7 @@ export class ModerationViews {
actions,
},
invites: inviteCodes,
labels,
}
}
@ -239,10 +242,11 @@ export class ModerationViews {
.selectAll()
.execute(),
])
const [reports, actions, blobs] = await Promise.all([
const [reports, actions, blobs, labels] = await Promise.all([
this.report(reportResults),
this.action(actionResults),
this.blob(record.blobCids),
this.labels(record.uri),
])
return {
...record,
@ -252,6 +256,7 @@ export class ModerationViews {
reports,
actions,
},
labels,
}
}
@ -314,6 +319,8 @@ export class ModerationViews {
reason: res.reason,
createdAt: res.createdAt,
createdBy: res.createdBy,
createLabelVals: res.createLabelVals?.split(' '),
negateLabelVals: res.negateLabelVals?.split(' '),
reversal:
res.reversedAt !== null &&
res.reversedBy !== null &&
@ -350,6 +357,8 @@ export class ModerationViews {
action: action.action,
subject,
subjectBlobs,
createLabelVals: action.createLabelVals,
negateLabelVals: action.negateLabelVals,
reason: action.reason,
createdAt: action.createdAt,
createdBy: action.createdBy,
@ -538,6 +547,21 @@ export class ModerationViews {
}
})
}
// @TODO: call into label service instead on AppView
async labels(subject: string, includeNeg?: boolean): Promise<Label[]> {
const res = await this.db.db
.selectFrom('label')
.where('label.uri', '=', subject)
.if(!includeNeg, (qb) => qb.where('neg', '=', 0))
.selectAll()
.execute()
return res.map((l) => ({
...l,
cid: l.cid === '' ? undefined : l.cid,
neg: l.neg === 1,
}))
}
}
type RepoResult = DidHandle & RepoRoot

@ -5,7 +5,7 @@ Object {
"action": "com.atproto.admin.defs#takedown",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 1,
"id": 2,
"reason": "Y",
"resolvedReportIds": Array [
6,

@ -33,7 +33,15 @@ Array [
"by": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(1)",
"muted": false,
@ -47,7 +55,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(1)",
"muted": false,
@ -622,7 +638,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(1)",
"muted": false,
@ -821,7 +845,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(1)",
"muted": false,
@ -982,7 +1014,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(1)",
"muted": false,

@ -128,11 +128,12 @@ describe('labeler', () => {
await ctx.db.db
.insertInto('label')
.values({
sourceDid: labelerDid,
subjectUri: aliceDid,
value: 'repo-label',
negated: 0,
createdAt: new Date().toISOString(),
src: labelerDid,
uri: aliceDid,
cid: '',
val: 'repo-label',
neg: 0,
cts: new Date().toISOString(),
})
.execute()

@ -1,4 +1,4 @@
import AtpAgent from '@atproto/api'
import AtpAgent, { ComAtprotoAdminTakeModerationAction } from '@atproto/api'
import { AtUri } from '@atproto/uri'
import {
adminAuth,
@ -696,6 +696,172 @@ describe('moderation', () => {
},
)
})
it('negates an existing label and reverses.', async () => {
const { ctx } = server
const post = sc.posts[sc.dids.bob][0].ref
const labelingService = ctx.services.appView.label(ctx.db)
await labelingService.formatAndCreate(
ctx.cfg.labelerDid,
post.uriStr,
post.cidStr,
{ create: ['kittens'] },
)
const action = await actionWithLabels({
negateLabelVals: ['kittens'],
subject: {
$type: 'com.atproto.repo.strongRef',
uri: post.uriStr,
cid: post.cidStr,
},
})
await expect(getRecordLabels(post.uriStr)).resolves.toEqual([])
await reverse(action.id)
await expect(getRecordLabels(post.uriStr)).resolves.toEqual(['kittens'])
// Cleanup
await labelingService.formatAndCreate(
ctx.cfg.labelerDid,
post.uriStr,
post.cidStr,
{ negate: ['kittens'] },
)
})
it('no-ops when negating an already-negated label and reverses.', async () => {
const { ctx } = server
const post = sc.posts[sc.dids.bob][0].ref
const labelingService = ctx.services.appView.label(ctx.db)
const action = await actionWithLabels({
negateLabelVals: ['bears'],
subject: {
$type: 'com.atproto.repo.strongRef',
uri: post.uriStr,
cid: post.cidStr,
},
})
await expect(getRecordLabels(post.uriStr)).resolves.toEqual([])
await reverse(action.id)
await expect(getRecordLabels(post.uriStr)).resolves.toEqual(['bears'])
// Cleanup
await labelingService.formatAndCreate(
ctx.cfg.labelerDid,
post.uriStr,
post.cidStr,
{ negate: ['bears'] },
)
})
it('creates a non-existing label and reverses.', async () => {
const post = sc.posts[sc.dids.bob][0].ref
const action = await actionWithLabels({
createLabelVals: ['puppies'],
subject: {
$type: 'com.atproto.repo.strongRef',
uri: post.uriStr,
cid: post.cidStr,
},
})
await expect(getRecordLabels(post.uriStr)).resolves.toEqual(['puppies'])
await reverse(action.id)
await expect(getRecordLabels(post.uriStr)).resolves.toEqual([])
})
it('no-ops when creating an existing label and reverses.', async () => {
const { ctx } = server
const post = sc.posts[sc.dids.bob][0].ref
const labelingService = ctx.services.appView.label(ctx.db)
await labelingService.formatAndCreate(
ctx.cfg.labelerDid,
post.uriStr,
post.cidStr,
{ create: ['birds'] },
)
const action = await actionWithLabels({
createLabelVals: ['birds'],
subject: {
$type: 'com.atproto.repo.strongRef',
uri: post.uriStr,
cid: post.cidStr,
},
})
await expect(getRecordLabels(post.uriStr)).resolves.toEqual(['birds'])
await reverse(action.id)
await expect(getRecordLabels(post.uriStr)).resolves.toEqual([])
})
it('creates and negates labels on a repo.', async () => {
const { ctx } = server
const labelingService = ctx.services.appView.label(ctx.db)
await labelingService.formatAndCreate(
ctx.cfg.labelerDid,
sc.dids.bob,
null,
{ create: ['kittens'] },
)
const action = await actionWithLabels({
createLabelVals: ['puppies'],
negateLabelVals: ['kittens'],
subject: {
$type: 'com.atproto.admin.defs#repoRef',
did: sc.dids.bob,
},
})
await expect(getRepoLabels(sc.dids.bob)).resolves.toEqual(['puppies'])
await reverse(action.id)
await expect(getRepoLabels(sc.dids.bob)).resolves.toEqual(['kittens'])
})
async function actionWithLabels(
opts: Partial<ComAtprotoAdminTakeModerationAction.InputSchema> & {
subject: ComAtprotoAdminTakeModerationAction.InputSchema['subject']
},
) {
const result = await agent.api.com.atproto.admin.takeModerationAction(
{
action: FLAG,
createdBy: 'did:example:admin',
reason: 'Y',
...opts,
},
{
encoding: 'application/json',
headers: { authorization: adminAuth() },
},
)
return result.data
}
async function reverse(actionId: number) {
await agent.api.com.atproto.admin.reverseModerationAction(
{
id: actionId,
createdBy: 'did:example:admin',
reason: 'Y',
},
{
encoding: 'application/json',
headers: { authorization: adminAuth() },
},
)
}
async function getRecordLabels(uri: string) {
const result = await agent.api.com.atproto.admin.getRecord(
{ uri },
{ headers: { authorization: adminAuth() } },
)
const labels = result.data.labels ?? []
return labels.map((l) => l.val)
}
async function getRepoLabels(did: string) {
const result = await agent.api.com.atproto.admin.getRepo(
{ did },
{ headers: { authorization: adminAuth() } },
)
const labels = result.data.labels ?? []
return labels.map((l) => l.val)
}
})
describe('blob takedown', () => {

@ -1,4 +1,6 @@
import { ids } from '../../src/lexicon/lexicons'
import { FLAG } from '../../src/lexicon/types/com/atproto/admin/defs'
import { adminAuth } from '../_util'
import { SeedClient } from './client'
import usersSeed from './users'
@ -99,6 +101,23 @@ export default async (sc: SeedClient) => {
await sc.repost(carol, sc.posts[dan][1].ref)
await sc.repost(dan, sc.posts[alice][1].ref)
await sc.agent.com.atproto.admin.takeModerationAction(
{
action: FLAG,
subject: {
$type: 'com.atproto.admin.defs#repoRef',
did: dan,
},
createdBy: 'did:example:admin',
reason: 'test',
createLabelVals: ['repo-action-label'],
},
{
encoding: 'application/json',
headers: { authorization: adminAuth() },
},
)
return sc
}

@ -175,7 +175,15 @@ Array [
"author": Object {
"did": "user(2)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(2)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(8)",
"muted": false,
@ -594,7 +602,15 @@ Array [
"author": Object {
"did": "user(0)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(0)",
"val": "repo-action-label",
},
],
"viewer": Object {
"muted": false,
},
@ -1008,7 +1024,15 @@ Array [
"by": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"muted": false,
},
@ -1021,7 +1045,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"muted": false,
},
@ -1178,7 +1210,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"muted": false,
},
@ -1208,7 +1248,15 @@ Array [
"author": Object {
"did": "user(0)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(0)",
"val": "repo-action-label",
},
],
"viewer": Object {
"followedBy": "record(1)",
"muted": true,
@ -1366,7 +1414,15 @@ Array [
"author": Object {
"did": "user(0)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(0)",
"val": "repo-action-label",
},
],
"viewer": Object {
"followedBy": "record(1)",
"muted": true,
@ -1572,7 +1628,15 @@ Array [
"author": Object {
"did": "user(2)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(2)",
"val": "repo-action-label",
},
],
"viewer": Object {
"muted": false,
},

@ -20,7 +20,15 @@ Object {
"actor": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(1)",
"muted": false,

@ -40,7 +40,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(6)",
"muted": false,
@ -146,7 +154,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(6)",
"muted": false,
@ -605,7 +621,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(6)",
"muted": false,
@ -745,7 +769,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(6)",
"muted": false,
@ -905,7 +937,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(6)",
"muted": false,
@ -1046,7 +1086,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(6)",
"muted": false,
@ -1186,7 +1234,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(6)",
"muted": false,
@ -1346,7 +1402,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(6)",
"muted": false,

@ -8,7 +8,15 @@ Object {
"followsCount": 1,
"handle": "dan.test",
"indexedAt": "1970-01-01T00:00:00.000Z",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(0)",
"val": "repo-action-label",
},
],
"postsCount": 2,
"viewer": Object {
"muted": false,
@ -76,7 +84,15 @@ Array [
"followersCount": 1,
"followsCount": 1,
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(3)",
"val": "repo-action-label",
},
],
"postsCount": 2,
"viewer": Object {
"followedBy": "record(4)",
@ -112,7 +128,15 @@ Object {
"followersCount": 1,
"followsCount": 1,
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(0)",
"val": "repo-action-label",
},
],
"postsCount": 2,
"viewer": Object {
"followedBy": "record(0)",

@ -13,7 +13,15 @@ Array [
Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(0)",
"muted": false,
@ -68,7 +76,15 @@ Array [
Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(0)",
"muted": false,

@ -17,165 +17,37 @@ Array [
"cid": "cids(0)",
"indexedAt": "1970-01-01T00:00:00.000Z",
"labels": Array [],
"likeCount": 0,
"likeCount": 3,
"record": Object {
"$type": "app.bsky.feed.post",
"createdAt": "1970-01-01T00:00:00.000Z",
"reply": Object {
"parent": Object {
"cid": "cids(2)",
"uri": "record(2)",
},
"root": Object {
"cid": "cids(1)",
"uri": "record(1)",
},
},
"text": "thanks bob",
"text": "again",
},
"replyCount": 0,
"repostCount": 0,
"replyCount": 2,
"repostCount": 1,
"uri": "record(0)",
"viewer": Object {},
},
},
Object {
"post": Object {
"author": Object {
"reason": Object {
"$type": "app.bsky.feed.defs#reasonRepost",
"by": Object {
"did": "user(1)",
"handle": "carol.test",
"labels": Array [],
"handle": "dan.test",
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"followedBy": "record(5)",
"following": "record(4)",
"following": "record(1)",
"muted": false,
},
},
"cid": "cids(3)",
"indexedAt": "1970-01-01T00:00:00.000Z",
"labels": Array [],
"likeCount": 0,
"record": Object {
"$type": "app.bsky.feed.post",
"createdAt": "1970-01-01T00:00:00.000Z",
"reply": Object {
"parent": Object {
"cid": "cids(1)",
"uri": "record(1)",
},
"root": Object {
"cid": "cids(1)",
"uri": "record(1)",
},
},
"text": "of course",
},
"replyCount": 0,
"repostCount": 0,
"uri": "record(3)",
"viewer": Object {},
},
"reply": Object {
"parent": Object {
"author": Object {
"avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg",
"did": "user(0)",
"displayName": "ali",
"handle": "alice.test",
"labels": Array [],
"viewer": Object {
"muted": false,
},
},
"cid": "cids(1)",
"indexedAt": "1970-01-01T00:00:00.000Z",
"labels": Array [],
"likeCount": 3,
"record": Object {
"$type": "app.bsky.feed.post",
"createdAt": "1970-01-01T00:00:00.000Z",
"text": "again",
},
"replyCount": 2,
"repostCount": 1,
"uri": "record(1)",
"viewer": Object {},
},
"root": Object {
"author": Object {
"avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg",
"did": "user(0)",
"displayName": "ali",
"handle": "alice.test",
"labels": Array [],
"viewer": Object {
"muted": false,
},
},
"cid": "cids(1)",
"indexedAt": "1970-01-01T00:00:00.000Z",
"labels": Array [],
"likeCount": 3,
"record": Object {
"$type": "app.bsky.feed.post",
"createdAt": "1970-01-01T00:00:00.000Z",
"text": "again",
},
"replyCount": 2,
"repostCount": 1,
"uri": "record(1)",
"viewer": Object {},
},
},
},
Object {
"post": Object {
"author": Object {
"avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg",
"did": "user(0)",
"displayName": "ali",
"handle": "alice.test",
"labels": Array [],
"viewer": Object {
"muted": false,
},
},
"cid": "cids(4)",
"embed": Object {
"$type": "app.bsky.embed.record#view",
"record": Object {
"$type": "app.bsky.embed.record#viewNotFound",
"uri": "record(7)",
},
},
"indexedAt": "1970-01-01T00:00:00.000Z",
"labels": Array [
Object {
"cid": "cids(4)",
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "record(6)",
"val": "test-label",
},
],
"likeCount": 2,
"record": Object {
"$type": "app.bsky.feed.post",
"createdAt": "1970-01-01T00:00:00.000Z",
"embed": Object {
"$type": "app.bsky.embed.record",
"record": Object {
"cid": "cids(5)",
"uri": "record(7)",
},
},
"text": "yoohoo label_me",
},
"replyCount": 0,
"repostCount": 0,
"uri": "record(6)",
"viewer": Object {},
},
},
Object {
@ -193,105 +65,26 @@ Array [
"cid": "cids(1)",
"indexedAt": "1970-01-01T00:00:00.000Z",
"labels": Array [],
"likeCount": 3,
"likeCount": 0,
"record": Object {
"$type": "app.bsky.feed.post",
"createdAt": "1970-01-01T00:00:00.000Z",
"text": "again",
},
"replyCount": 2,
"repostCount": 1,
"uri": "record(1)",
"viewer": Object {},
},
},
Object {
"post": Object {
"author": Object {
"did": "user(1)",
"handle": "carol.test",
"labels": Array [],
"viewer": Object {
"followedBy": "record(5)",
"following": "record(4)",
"muted": false,
},
},
"cid": "cids(6)",
"embed": Object {
"$type": "app.bsky.embed.recordWithMedia#view",
"media": Object {
"$type": "app.bsky.embed.images#view",
"images": Array [
Object {
"alt": "tests/image/fixtures/key-landscape-small.jpg",
"fullsize": "https://pds.public.url/image/AiDXkxVbgBksxb1nfiRn1m6S4K8_mee6o8r-UGLNzOM/rs:fit:2000:2000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg",
"thumb": "https://pds.public.url/image/uc7FGfiGv0mMqmk9XiqHXrIhNymLHaex7Ge8nEhmXqo/rs:fit:1000:1000:1:0/plain/bafkreigy5p3xxceipk2o6nqtnugpft26ol6yleqhboqziino7axvdngtci@jpeg",
},
Object {
"alt": "tests/image/fixtures/key-alt.jpg",
"fullsize": "https://pds.public.url/image/xC2No-8rKVDIwIMmCiEBm9EiGLDBBOpf36PHoGf-GDw/rs:fit:2000:2000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg",
"thumb": "https://pds.public.url/image/g7yazUpNwN8LKumZ2Zmn_ptQbtMLs1Pti5-GDn7H8_8/rs:fit:1000:1000:1:0/plain/bafkreifdklbbcdsyanjz3oqe5pf2omuq5ansthokxlbleagg3eenx62h7e@jpeg",
},
],
},
"record": Object {
"record": Object {
"$type": "app.bsky.embed.record#viewNotFound",
"uri": "record(9)",
"reply": Object {
"parent": Object {
"cid": "cids(2)",
"uri": "record(3)",
},
"root": Object {
"cid": "cids(0)",
"uri": "record(0)",
},
},
},
"indexedAt": "1970-01-01T00:00:00.000Z",
"labels": Array [],
"likeCount": 2,
"record": Object {
"$type": "app.bsky.feed.post",
"createdAt": "1970-01-01T00:00:00.000Z",
"embed": Object {
"$type": "app.bsky.embed.recordWithMedia",
"media": Object {
"$type": "app.bsky.embed.images",
"images": Array [
Object {
"alt": "tests/image/fixtures/key-landscape-small.jpg",
"image": Object {
"$type": "blob",
"mimeType": "image/jpeg",
"ref": Object {
"$link": "cids(7)",
},
"size": 4114,
},
},
Object {
"alt": "tests/image/fixtures/key-alt.jpg",
"image": Object {
"$type": "blob",
"mimeType": "image/jpeg",
"ref": Object {
"$link": "cids(8)",
},
"size": 12736,
},
},
],
},
"record": Object {
"record": Object {
"cid": "cids(9)",
"uri": "record(9)",
},
},
},
"text": "hi im carol",
"text": "thanks bob",
},
"replyCount": 0,
"repostCount": 0,
"uri": "record(8)",
"viewer": Object {
"like": "record(10)",
},
"uri": "record(2)",
"viewer": Object {},
},
},
Object {
@ -306,7 +99,234 @@ Array [
"muted": false,
},
},
"cid": "cids(10)",
"cid": "cids(3)",
"embed": Object {
"$type": "app.bsky.embed.record#view",
"record": Object {
"$type": "app.bsky.embed.record#viewRecord",
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(1)",
"muted": false,
},
},
"cid": "cids(4)",
"embeds": Array [
Object {
"$type": "app.bsky.embed.record#view",
"record": Object {
"$type": "app.bsky.embed.record#viewNotFound",
"uri": "record(6)",
},
},
],
"indexedAt": "1970-01-01T00:00:00.000Z",
"uri": "record(5)",
"value": Object {
"$type": "app.bsky.feed.post",
"createdAt": "1970-01-01T00:00:00.000Z",
"embed": Object {
"$type": "app.bsky.embed.record",
"record": Object {
"cid": "cids(5)",
"uri": "record(6)",
},
},
"facets": Array [
Object {
"features": Array [
Object {
"$type": "app.bsky.richtext.facet#mention",
"did": "user(0)",
},
],
"index": Object {
"byteEnd": 18,
"byteStart": 0,
},
},
],
"text": "@alice.bluesky.xyz is the best",
},
},
},
"indexedAt": "1970-01-01T00:00:00.000Z",
"labels": Array [
Object {
"cid": "cids(3)",
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "record(4)",
"val": "test-label",
},
],
"likeCount": 2,
"record": Object {
"$type": "app.bsky.feed.post",
"createdAt": "1970-01-01T00:00:00.000Z",
"embed": Object {
"$type": "app.bsky.embed.record",
"record": Object {
"cid": "cids(4)",
"uri": "record(5)",
},
},
"text": "yoohoo label_me",
},
"replyCount": 0,
"repostCount": 0,
"uri": "record(4)",
"viewer": Object {},
},
},
Object {
"post": Object {
"author": Object {
"avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg",
"did": "user(0)",
"displayName": "ali",
"handle": "alice.test",
"labels": Array [],
"viewer": Object {
"muted": false,
},
},
"cid": "cids(0)",
"indexedAt": "1970-01-01T00:00:00.000Z",
"labels": Array [],
"likeCount": 3,
"record": Object {
"$type": "app.bsky.feed.post",
"createdAt": "1970-01-01T00:00:00.000Z",
"text": "again",
},
"replyCount": 2,
"repostCount": 1,
"uri": "record(0)",
"viewer": Object {},
},
},
Object {
"post": Object {
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(1)",
"muted": false,
},
},
"cid": "cids(4)",
"embed": Object {
"$type": "app.bsky.embed.record#view",
"record": Object {
"$type": "app.bsky.embed.record#viewNotFound",
"uri": "record(6)",
},
},
"indexedAt": "1970-01-01T00:00:00.000Z",
"labels": Array [],
"likeCount": 0,
"record": Object {
"$type": "app.bsky.feed.post",
"createdAt": "1970-01-01T00:00:00.000Z",
"embed": Object {
"$type": "app.bsky.embed.record",
"record": Object {
"cid": "cids(5)",
"uri": "record(6)",
},
},
"facets": Array [
Object {
"features": Array [
Object {
"$type": "app.bsky.richtext.facet#mention",
"did": "user(0)",
},
],
"index": Object {
"byteEnd": 18,
"byteStart": 0,
},
},
],
"text": "@alice.bluesky.xyz is the best",
},
"replyCount": 0,
"repostCount": 1,
"uri": "record(5)",
"viewer": Object {},
},
},
Object {
"post": Object {
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(1)",
"muted": false,
},
},
"cid": "cids(6)",
"indexedAt": "1970-01-01T00:00:00.000Z",
"labels": Array [],
"likeCount": 0,
"record": Object {
"$type": "app.bsky.feed.post",
"createdAt": "1970-01-01T00:00:00.000Z",
"text": "dan here!",
},
"replyCount": 0,
"repostCount": 0,
"uri": "record(7)",
"viewer": Object {},
},
},
Object {
"post": Object {
"author": Object {
"avatar": "https://pds.public.url/image/KzkHFsMRQ6oAKCHCRKFA1H-rDdc7VOtvEVpUJ82TwyQ/rs:fill:1000:1000:1:0/plain/bafkreiaivizp4xldojmmpuzmiu75cmea7nq56dnntnuhzhsjcb63aou5ei@jpeg",
"did": "user(0)",
"displayName": "ali",
"handle": "alice.test",
"labels": Array [],
"viewer": Object {
"muted": false,
},
},
"cid": "cids(7)",
"indexedAt": "1970-01-01T00:00:00.000Z",
"labels": Array [],
"likeCount": 0,
@ -317,7 +337,7 @@ Array [
},
"replyCount": 0,
"repostCount": 0,
"uri": "record(11)",
"uri": "record(8)",
"viewer": Object {},
},
},
@ -357,7 +377,15 @@ Array [
"by": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(1)",
"muted": false,
@ -612,7 +640,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(1)",
"muted": false,
@ -852,7 +888,15 @@ Array [
"by": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(1)",
"muted": false,
@ -866,7 +910,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(1)",
"muted": false,
@ -1441,7 +1493,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(1)",
"muted": false,
@ -1640,7 +1700,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(1)",
"muted": false,
@ -1801,7 +1869,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(1)",
"muted": false,
@ -2015,7 +2091,15 @@ Array [
"author": Object {
"did": "user(0)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(0)",
"val": "repo-action-label",
},
],
"viewer": Object {
"followedBy": "record(1)",
"muted": false,
@ -2605,7 +2689,15 @@ Array [
"author": Object {
"did": "user(0)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(0)",
"val": "repo-action-label",
},
],
"viewer": Object {
"followedBy": "record(1)",
"muted": false,
@ -2992,7 +3084,15 @@ Array [
"author": Object {
"did": "user(0)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(0)",
"val": "repo-action-label",
},
],
"viewer": Object {
"muted": false,
},
@ -3433,7 +3533,15 @@ Array [
"author": Object {
"did": "user(0)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(0)",
"val": "repo-action-label",
},
],
"viewer": Object {
"muted": false,
},
@ -3774,7 +3882,15 @@ Array [
"by": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"muted": false,
},
@ -3974,7 +4090,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"muted": false,
},
@ -4131,7 +4255,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"muted": false,
},
@ -4224,7 +4356,15 @@ Array [
"by": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(1)",
"muted": false,
@ -4406,7 +4546,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(1)",
"muted": false,
@ -4567,7 +4715,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(1)",
"muted": false,
@ -4728,7 +4884,15 @@ Array [
"author": Object {
"did": "user(1)",
"handle": "dan.test",
"labels": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(1)",
"val": "repo-action-label",
},
],
"viewer": Object {
"following": "record(1)",
"muted": false,

@ -5,7 +5,7 @@ Object {
"action": "com.atproto.admin.defs#takedown",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 2,
"id": 3,
"reason": "X",
"resolvedReports": Array [
Object {
@ -15,8 +15,8 @@ Object {
"reasonType": "com.atproto.moderation.defs#reasonOther",
"reportedBy": "user(1)",
"resolvedByActionIds": Array [
3,
2,
1,
],
"subject": Object {
"$type": "com.atproto.repo.strongRef",
@ -33,7 +33,7 @@ Object {
"moderation": Object {
"currentAction": Object {
"action": "com.atproto.admin.defs#takedown",
"id": 2,
"id": 3,
},
},
"repo": Object {
@ -76,7 +76,7 @@ Object {
"action": "com.atproto.admin.defs#flag",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 1,
"id": 2,
"reason": "X",
"resolvedReports": Array [
Object {
@ -86,8 +86,8 @@ Object {
"reasonType": "com.atproto.moderation.defs#reasonOther",
"reportedBy": "user(1)",
"resolvedByActionIds": Array [
3,
2,
1,
],
"subject": Object {
"$type": "com.atproto.repo.strongRef",
@ -101,7 +101,7 @@ Object {
"reasonType": "com.atproto.moderation.defs#reasonSpam",
"reportedBy": "user(2)",
"resolvedByActionIds": Array [
1,
2,
],
"subject": Object {
"$type": "com.atproto.admin.defs#repoRef",

@ -6,7 +6,7 @@ Array [
"action": "com.atproto.admin.defs#flag",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 1,
"id": 2,
"reason": "X",
"resolvedReportIds": Array [
1,
@ -32,7 +32,7 @@ Array [
"action": "com.atproto.admin.defs#flag",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 5,
"id": 6,
"reason": "X",
"resolvedReportIds": Array [
3,
@ -47,7 +47,7 @@ Array [
"action": "com.atproto.admin.defs#acknowledge",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 2,
"id": 3,
"reason": "X",
"resolvedReportIds": Array [],
"subject": Object {
@ -61,7 +61,7 @@ Array [
"action": "com.atproto.admin.defs#flag",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 1,
"id": 2,
"reason": "X",
"resolvedReportIds": Array [
1,
@ -87,7 +87,7 @@ Array [
"action": "com.atproto.admin.defs#acknowledge",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 6,
"id": 7,
"reason": "X",
"resolvedReportIds": Array [],
"subject": Object {
@ -100,7 +100,7 @@ Array [
"action": "com.atproto.admin.defs#flag",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 5,
"id": 6,
"reason": "X",
"resolvedReportIds": Array [
3,
@ -115,7 +115,7 @@ Array [
"action": "com.atproto.admin.defs#flag",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 4,
"id": 5,
"reason": "X",
"resolvedReportIds": Array [],
"subject": Object {
@ -129,7 +129,7 @@ Array [
"action": "com.atproto.admin.defs#takedown",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 3,
"id": 4,
"reason": "X",
"resolvedReportIds": Array [],
"subject": Object {
@ -143,7 +143,7 @@ Array [
"action": "com.atproto.admin.defs#acknowledge",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 2,
"id": 3,
"reason": "X",
"resolvedReportIds": Array [],
"subject": Object {
@ -157,7 +157,7 @@ Array [
"action": "com.atproto.admin.defs#flag",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 1,
"id": 2,
"reason": "X",
"resolvedReportIds": Array [
1,
@ -174,5 +174,21 @@ Array [
},
"subjectBlobCids": Array [],
},
Object {
"action": "com.atproto.admin.defs#flag",
"createLabelVals": Array [
"repo-action-label",
],
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 1,
"reason": "test",
"resolvedReportIds": Array [],
"subject": Object {
"$type": "com.atproto.admin.defs#repoRef",
"did": "user(2)",
},
"subjectBlobCids": Array [],
},
]
`;

@ -12,7 +12,7 @@ Object {
"action": "com.atproto.admin.defs#takedown",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 2,
"id": 3,
"reason": "X",
"resolvedReportIds": Array [
2,
@ -28,7 +28,7 @@ Object {
"action": "com.atproto.admin.defs#flag",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 1,
"id": 2,
"reason": "X",
"resolvedReportIds": Array [
2,
@ -54,7 +54,7 @@ Object {
"moderation": Object {
"currentAction": Object {
"action": "com.atproto.admin.defs#takedown",
"id": 2,
"id": 3,
},
},
"repo": Object {
@ -102,7 +102,7 @@ Object {
"action": "com.atproto.admin.defs#flag",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 1,
"id": 2,
"reason": "X",
"resolvedReportIds": Array [
2,

@ -8,7 +8,7 @@ Array [
"reasonType": "com.atproto.moderation.defs#reasonOther",
"reportedBy": "user(0)",
"resolvedByActionIds": Array [
1,
2,
],
"subject": Object {
"$type": "com.atproto.repo.strongRef",
@ -27,7 +27,7 @@ Array [
"reasonType": "com.atproto.moderation.defs#reasonOther",
"reportedBy": "user(0)",
"resolvedByActionIds": Array [
5,
6,
],
"subject": Object {
"$type": "com.atproto.admin.defs#repoRef",
@ -52,7 +52,7 @@ Array [
"reasonType": "com.atproto.moderation.defs#reasonOther",
"reportedBy": "user(0)",
"resolvedByActionIds": Array [
1,
2,
],
"subject": Object {
"$type": "com.atproto.repo.strongRef",
@ -82,7 +82,7 @@ Array [
"reasonType": "com.atproto.moderation.defs#reasonOther",
"reportedBy": "user(1)",
"resolvedByActionIds": Array [
5,
6,
],
"subject": Object {
"$type": "com.atproto.admin.defs#repoRef",
@ -107,7 +107,7 @@ Array [
"reasonType": "com.atproto.moderation.defs#reasonOther",
"reportedBy": "user(1)",
"resolvedByActionIds": Array [
3,
4,
],
"subject": Object {
"$type": "com.atproto.repo.strongRef",
@ -133,7 +133,7 @@ Array [
"reasonType": "com.atproto.moderation.defs#reasonOther",
"reportedBy": "user(1)",
"resolvedByActionIds": Array [
1,
2,
],
"subject": Object {
"$type": "com.atproto.repo.strongRef",
@ -152,7 +152,7 @@ Array [
"reasonType": "com.atproto.moderation.defs#reasonOther",
"reportedBy": "user(0)",
"resolvedByActionIds": Array [
5,
6,
],
"subject": Object {
"$type": "com.atproto.admin.defs#repoRef",
@ -165,7 +165,7 @@ Array [
"reasonType": "com.atproto.moderation.defs#reasonOther",
"reportedBy": "user(0)",
"resolvedByActionIds": Array [
3,
4,
],
"subject": Object {
"$type": "com.atproto.repo.strongRef",
@ -179,7 +179,7 @@ Array [
"reasonType": "com.atproto.moderation.defs#reasonOther",
"reportedBy": "user(0)",
"resolvedByActionIds": Array [
1,
2,
],
"subject": Object {
"$type": "com.atproto.repo.strongRef",

@ -6,13 +6,14 @@ Object {
"blobs": Array [],
"cid": "cids(0)",
"indexedAt": "1970-01-01T00:00:00.000Z",
"labels": Array [],
"moderation": Object {
"actions": Array [
Object {
"action": "com.atproto.admin.defs#takedown",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 2,
"id": 3,
"reason": "X",
"resolvedReportIds": Array [],
"subject": Object {
@ -26,7 +27,7 @@ Object {
"action": "com.atproto.admin.defs#acknowledge",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 1,
"id": 2,
"reason": "X",
"resolvedReportIds": Array [],
"reversal": Object {
@ -44,7 +45,7 @@ Object {
],
"currentAction": Object {
"action": "com.atproto.admin.defs#takedown",
"id": 2,
"id": 3,
},
"reports": Array [
Object {
@ -113,13 +114,14 @@ Object {
"blobs": Array [],
"cid": "cids(0)",
"indexedAt": "1970-01-01T00:00:00.000Z",
"labels": Array [],
"moderation": Object {
"actions": Array [
Object {
"action": "com.atproto.admin.defs#takedown",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 2,
"id": 3,
"reason": "X",
"resolvedReportIds": Array [],
"subject": Object {
@ -133,7 +135,7 @@ Object {
"action": "com.atproto.admin.defs#acknowledge",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 1,
"id": 2,
"reason": "X",
"resolvedReportIds": Array [],
"reversal": Object {
@ -151,7 +153,132 @@ Object {
],
"currentAction": Object {
"action": "com.atproto.admin.defs#takedown",
"id": 2,
"id": 3,
},
"reports": Array [
Object {
"createdAt": "1970-01-01T00:00:00.000Z",
"id": 2,
"reason": "defamation",
"reasonType": "com.atproto.moderation.defs#reasonOther",
"reportedBy": "user(1)",
"resolvedByActionIds": Array [],
"subject": Object {
"$type": "com.atproto.repo.strongRef",
"cid": "cids(0)",
"uri": "record(0)",
},
},
Object {
"createdAt": "1970-01-01T00:00:00.000Z",
"id": 1,
"reasonType": "com.atproto.moderation.defs#reasonSpam",
"reportedBy": "user(2)",
"resolvedByActionIds": Array [],
"subject": Object {
"$type": "com.atproto.repo.strongRef",
"cid": "cids(0)",
"uri": "record(0)",
},
},
],
},
"repo": Object {
"account": Object {
"email": "alice@test.com",
},
"did": "user(0)",
"handle": "alice.test",
"indexedAt": "1970-01-01T00:00:00.000Z",
"moderation": Object {},
"relatedRecords": Array [
Object {
"$type": "app.bsky.actor.profile",
"avatar": Object {
"$type": "blob",
"mimeType": "image/jpeg",
"ref": Object {
"$link": "cids(1)",
},
"size": 3976,
},
"description": "its me!",
"displayName": "ali",
},
],
},
"uri": "record(0)",
"value": Object {
"$type": "app.bsky.feed.post",
"createdAt": "1970-01-01T00:00:00.000Z",
"text": "hey there",
},
}
`;
exports[`pds admin get record view serves labels. 1`] = `
Object {
"blobCids": Array [],
"blobs": Array [],
"cid": "cids(0)",
"indexedAt": "1970-01-01T00:00:00.000Z",
"labels": Array [
Object {
"cid": "cids(0)",
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "record(0)",
"val": "kittens",
},
Object {
"cid": "cids(0)",
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "record(0)",
"val": "puppies",
},
],
"moderation": Object {
"actions": Array [
Object {
"action": "com.atproto.admin.defs#takedown",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 3,
"reason": "X",
"resolvedReportIds": Array [],
"subject": Object {
"$type": "com.atproto.repo.strongRef",
"cid": "cids(0)",
"uri": "record(0)",
},
"subjectBlobCids": Array [],
},
Object {
"action": "com.atproto.admin.defs#acknowledge",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 2,
"reason": "X",
"resolvedReportIds": Array [],
"reversal": Object {
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"reason": "X",
},
"subject": Object {
"$type": "com.atproto.repo.strongRef",
"cid": "cids(0)",
"uri": "record(0)",
},
"subjectBlobCids": Array [],
},
],
"currentAction": Object {
"action": "com.atproto.admin.defs#takedown",
"id": 3,
},
"reports": Array [
Object {

@ -9,13 +9,14 @@ Object {
"handle": "alice.test",
"indexedAt": "1970-01-01T00:00:00.000Z",
"invites": Array [],
"labels": Array [],
"moderation": Object {
"actions": Array [
Object {
"action": "com.atproto.admin.defs#takedown",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 2,
"id": 3,
"reason": "X",
"resolvedReportIds": Array [],
"subject": Object {
@ -28,7 +29,7 @@ Object {
"action": "com.atproto.admin.defs#acknowledge",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 1,
"id": 2,
"reason": "X",
"resolvedReportIds": Array [],
"reversal": Object {
@ -45,7 +46,114 @@ Object {
],
"currentAction": Object {
"action": "com.atproto.admin.defs#takedown",
"id": 2,
"id": 3,
},
"reports": Array [
Object {
"createdAt": "1970-01-01T00:00:00.000Z",
"id": 2,
"reason": "defamation",
"reasonType": "com.atproto.moderation.defs#reasonOther",
"reportedBy": "user(1)",
"resolvedByActionIds": Array [],
"subject": Object {
"$type": "com.atproto.admin.defs#repoRef",
"did": "user(0)",
},
},
Object {
"createdAt": "1970-01-01T00:00:00.000Z",
"id": 1,
"reasonType": "com.atproto.moderation.defs#reasonSpam",
"reportedBy": "user(2)",
"resolvedByActionIds": Array [],
"subject": Object {
"$type": "com.atproto.admin.defs#repoRef",
"did": "user(0)",
},
},
],
},
"relatedRecords": Array [
Object {
"$type": "app.bsky.actor.profile",
"avatar": Object {
"$type": "blob",
"mimeType": "image/jpeg",
"ref": Object {
"$link": "cids(0)",
},
"size": 3976,
},
"description": "its me!",
"displayName": "ali",
},
],
}
`;
exports[`pds admin get repo view serves labels. 1`] = `
Object {
"account": Object {
"email": "alice@test.com",
},
"did": "user(0)",
"handle": "alice.test",
"indexedAt": "1970-01-01T00:00:00.000Z",
"invites": Array [],
"labels": Array [
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(0)",
"val": "kittens",
},
Object {
"cts": "1970-01-01T00:00:00.000Z",
"neg": false,
"src": "did:example:labeler",
"uri": "user(0)",
"val": "puppies",
},
],
"moderation": Object {
"actions": Array [
Object {
"action": "com.atproto.admin.defs#takedown",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 3,
"reason": "X",
"resolvedReportIds": Array [],
"subject": Object {
"$type": "com.atproto.admin.defs#repoRef",
"did": "user(0)",
},
"subjectBlobCids": Array [],
},
Object {
"action": "com.atproto.admin.defs#acknowledge",
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"id": 2,
"reason": "X",
"resolvedReportIds": Array [],
"reversal": Object {
"createdAt": "1970-01-01T00:00:00.000Z",
"createdBy": "did:example:admin",
"reason": "X",
},
"subject": Object {
"$type": "com.atproto.admin.defs#repoRef",
"did": "user(0)",
},
"subjectBlobCids": Array [],
},
],
"currentAction": Object {
"action": "com.atproto.admin.defs#takedown",
"id": 3,
},
"reports": Array [
Object {

@ -76,16 +76,18 @@ describe('pds admin get moderation action view', () => {
})
it('gets moderation action for a repo.', async () => {
// id 2 because id 1 is in seed client
const result = await agent.api.com.atproto.admin.getModerationAction(
{ id: 1 },
{ id: 2 },
{ headers: { authorization: adminAuth() } },
)
expect(forSnapshot(result.data)).toMatchSnapshot()
})
it('gets moderation action for a record.', async () => {
// id 3 because id 1 is in seed client
const result = await agent.api.com.atproto.admin.getModerationAction(
{ id: 2 },
{ id: 3 },
{ headers: { authorization: adminAuth() } },
)
expect(forSnapshot(result.data)).toMatchSnapshot()

@ -165,7 +165,7 @@ describe('pds admin get moderation actions view', () => {
{ headers: { authorization: adminAuth() } },
)
expect(full.data.actions.length).toEqual(6)
expect(full.data.actions.length).toEqual(7) // extra one because of seed client
expect(results(paginatedAll)).toEqual(results([full.data]))
})
})

@ -8,17 +8,24 @@ import {
REASONOTHER,
REASONSPAM,
} from '../../../src/lexicon/types/com/atproto/moderation/defs'
import { runTestServer, forSnapshot, CloseFn, adminAuth } from '../../_util'
import {
runTestServer,
forSnapshot,
CloseFn,
adminAuth,
TestServerInfo,
} from '../../_util'
import { SeedClient } from '../../seeds/client'
import basicSeed from '../../seeds/basic'
describe('pds admin get record view', () => {
let server: TestServerInfo
let agent: AtpAgent
let close: CloseFn
let sc: SeedClient
beforeAll(async () => {
const server = await runTestServer({
server = await runTestServer({
dbPostgresSchema: 'views_admin_get_record',
})
close = server.close
@ -113,4 +120,29 @@ describe('pds admin get record view', () => {
)
await expect(promise).rejects.toThrow('Record not found')
})
it('serves labels.', async () => {
const { ctx } = server
const labelingService = ctx.services.appView.label(ctx.db)
await labelingService.formatAndCreate(
ctx.cfg.labelerDid,
sc.posts[sc.dids.alice][0].ref.uriStr,
sc.posts[sc.dids.alice][0].ref.cidStr,
{ create: ['kittens', 'puppies', 'birds'] },
)
await labelingService.formatAndCreate(
ctx.cfg.labelerDid,
sc.posts[sc.dids.alice][0].ref.uriStr,
sc.posts[sc.dids.alice][0].ref.cidStr,
{ negate: ['birds'] },
)
const result = await agent.api.com.atproto.admin.getRecord(
{
uri: sc.posts[sc.dids.alice][0].ref.uriStr,
cid: sc.posts[sc.dids.alice][0].ref.cidStr,
},
{ headers: { authorization: adminAuth() } },
)
expect(forSnapshot(result.data)).toMatchSnapshot()
})
})

@ -7,17 +7,24 @@ import {
REASONOTHER,
REASONSPAM,
} from '../../../src/lexicon/types/com/atproto/moderation/defs'
import { runTestServer, forSnapshot, CloseFn, adminAuth } from '../../_util'
import {
runTestServer,
forSnapshot,
CloseFn,
adminAuth,
TestServerInfo,
} from '../../_util'
import { SeedClient } from '../../seeds/client'
import basicSeed from '../../seeds/basic'
describe('pds admin get repo view', () => {
let server: TestServerInfo
let agent: AtpAgent
let close: CloseFn
let sc: SeedClient
beforeAll(async () => {
const server = await runTestServer({
server = await runTestServer({
dbPostgresSchema: 'views_admin_get_repo',
})
close = server.close
@ -80,4 +87,26 @@ describe('pds admin get repo view', () => {
)
await expect(promise).rejects.toThrow('Repo not found')
})
it('serves labels.', async () => {
const { ctx } = server
const labelingService = ctx.services.appView.label(ctx.db)
await labelingService.formatAndCreate(
ctx.cfg.labelerDid,
sc.dids.alice,
null,
{ create: ['kittens', 'puppies', 'birds'] },
)
await labelingService.formatAndCreate(
ctx.cfg.labelerDid,
sc.dids.alice,
null,
{ negate: ['birds'] },
)
const result = await agent.api.com.atproto.admin.getRepo(
{ did: sc.dids.alice },
{ headers: { authorization: adminAuth() } },
)
expect(forSnapshot(result.data)).toMatchSnapshot()
})
})

@ -169,7 +169,7 @@ describe('timeline views', () => {
it('blocks posts, reposts, replies by actor takedown', async () => {
const actionResults = await Promise.all(
[bob, dan].map((did) =>
[bob, carol].map((did) =>
agent.api.com.atproto.admin.takeModerationAction(
{
action: TAKEDOWN,