Support proxying moderation through to appview (#1233)
* in-progress work on proxying moderation to appview * tidy * proxy reports pds to appview, misc tidy * test proxying of moderation endpoints * remove report action fkeys from pds for appview sync * test appview/pds moderation synchronization * tidy * tidy * fix admin proxy tests, build * temp disable migration * rm relative @atproto/api imports * fix a couple more relative imports * tidy * reenable migration, comment contents temporarily * fully enable migration, remove build, misc test fixes --------- Co-authored-by: dholms <dtholmgren@gmail.com>
This commit is contained in:
parent
43bb6ae147
commit
3f3ae4cfd5
packages
bsky/src/api/app/bsky/feed
pds
src
api/com/atproto
admin
getModerationAction.tsgetModerationActions.tsgetModerationReport.tsgetModerationReports.tsgetRecord.tsgetRepo.tsresolveModerationReports.tsreverseModerationAction.tssearchRepos.tstakeModerationAction.tsutil.ts
moderation
app-view/api/app/bsky/feed
config.tscontext.tsdb/migrations
tests
labeler
proxied
seeds
@ -1,7 +1,7 @@
|
||||
import * as common from '@atproto/common'
|
||||
import { Server } from '../../../../lexicon'
|
||||
import AppContext from '../../../../context'
|
||||
import { PostView } from '@atproto/api/src/client/types/app/bsky/feed/defs'
|
||||
import { PostView } from '../../../../lexicon/types/app/bsky/feed/defs'
|
||||
|
||||
export default function (server: Server, ctx: AppContext) {
|
||||
server.app.bsky.feed.getPosts({
|
||||
|
@ -1,10 +1,24 @@
|
||||
import { Server } from '../../../../lexicon'
|
||||
import AppContext from '../../../../context'
|
||||
import { authPassthru } from './util'
|
||||
|
||||
export default function (server: Server, ctx: AppContext) {
|
||||
server.com.atproto.admin.getModerationAction({
|
||||
auth: ctx.roleVerifier,
|
||||
handler: async ({ params, auth }) => {
|
||||
handler: async ({ req, params, auth }) => {
|
||||
if (ctx.shouldProxyModeration()) {
|
||||
// @TODO merge invite details into action subject
|
||||
const { data: result } =
|
||||
await ctx.appviewAgent.com.atproto.admin.getModerationAction(
|
||||
params,
|
||||
authPassthru(req),
|
||||
)
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: result,
|
||||
}
|
||||
}
|
||||
|
||||
const access = auth.credentials
|
||||
const { db, services } = ctx
|
||||
const { id } = params
|
||||
|
@ -1,10 +1,23 @@
|
||||
import { Server } from '../../../../lexicon'
|
||||
import AppContext from '../../../../context'
|
||||
import { authPassthru } from './util'
|
||||
|
||||
export default function (server: Server, ctx: AppContext) {
|
||||
server.com.atproto.admin.getModerationActions({
|
||||
auth: ctx.roleVerifier,
|
||||
handler: async ({ params }) => {
|
||||
handler: async ({ req, params }) => {
|
||||
if (ctx.shouldProxyModeration()) {
|
||||
const { data: result } =
|
||||
await ctx.appviewAgent.com.atproto.admin.getModerationActions(
|
||||
params,
|
||||
authPassthru(req),
|
||||
)
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: result,
|
||||
}
|
||||
}
|
||||
|
||||
const { db, services } = ctx
|
||||
const { subject, limit = 50, cursor } = params
|
||||
const moderationService = services.moderation(db)
|
||||
|
@ -1,10 +1,23 @@
|
||||
import { Server } from '../../../../lexicon'
|
||||
import AppContext from '../../../../context'
|
||||
import { authPassthru } from './util'
|
||||
|
||||
export default function (server: Server, ctx: AppContext) {
|
||||
server.com.atproto.admin.getModerationReport({
|
||||
auth: ctx.roleVerifier,
|
||||
handler: async ({ params, auth }) => {
|
||||
handler: async ({ req, params, auth }) => {
|
||||
if (ctx.shouldProxyModeration()) {
|
||||
const { data: result } =
|
||||
await ctx.appviewAgent.com.atproto.admin.getModerationReport(
|
||||
params,
|
||||
authPassthru(req),
|
||||
)
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: result,
|
||||
}
|
||||
}
|
||||
|
||||
const access = auth.credentials
|
||||
const { db, services } = ctx
|
||||
const { id } = params
|
||||
|
@ -1,10 +1,23 @@
|
||||
import { Server } from '../../../../lexicon'
|
||||
import AppContext from '../../../../context'
|
||||
import { authPassthru } from './util'
|
||||
|
||||
export default function (server: Server, ctx: AppContext) {
|
||||
server.com.atproto.admin.getModerationReports({
|
||||
auth: ctx.roleVerifier,
|
||||
handler: async ({ params }) => {
|
||||
handler: async ({ req, params }) => {
|
||||
if (ctx.shouldProxyModeration()) {
|
||||
const { data: result } =
|
||||
await ctx.appviewAgent.com.atproto.admin.getModerationReports(
|
||||
params,
|
||||
authPassthru(req),
|
||||
)
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: result,
|
||||
}
|
||||
}
|
||||
|
||||
const { db, services } = ctx
|
||||
const {
|
||||
subject,
|
||||
|
@ -1,26 +1,57 @@
|
||||
import { AtUri } from '@atproto/uri'
|
||||
import { InvalidRequestError } from '@atproto/xrpc-server'
|
||||
import { Server } from '../../../../lexicon'
|
||||
import AppContext from '../../../../context'
|
||||
import { AtUri } from '@atproto/uri'
|
||||
import { authPassthru, mergeRepoViewPdsDetails } from './util'
|
||||
|
||||
export default function (server: Server, ctx: AppContext) {
|
||||
server.com.atproto.admin.getRecord({
|
||||
auth: ctx.roleVerifier,
|
||||
handler: async ({ params, auth }) => {
|
||||
handler: async ({ req, params, auth }) => {
|
||||
const access = auth.credentials
|
||||
const { db, services } = ctx
|
||||
const { uri, cid } = params
|
||||
const result = await services
|
||||
.record(db)
|
||||
.getRecord(new AtUri(uri), cid ?? null, true)
|
||||
if (!result) {
|
||||
const recordDetail =
|
||||
result &&
|
||||
(await services.moderation(db).views.recordDetail(result, {
|
||||
includeEmails: access.moderator,
|
||||
}))
|
||||
|
||||
if (ctx.shouldProxyModeration()) {
|
||||
try {
|
||||
const { data: recordDetailAppview } =
|
||||
await ctx.appviewAgent.com.atproto.admin.getRecord(
|
||||
params,
|
||||
authPassthru(req),
|
||||
)
|
||||
if (recordDetail) {
|
||||
recordDetailAppview.repo = mergeRepoViewPdsDetails(
|
||||
recordDetailAppview.repo,
|
||||
recordDetail.repo,
|
||||
)
|
||||
}
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: recordDetailAppview,
|
||||
}
|
||||
} catch (err) {
|
||||
if (err && err['error'] === 'RecordNotFound') {
|
||||
throw new InvalidRequestError('Record not found', 'RecordNotFound')
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!recordDetail) {
|
||||
throw new InvalidRequestError('Record not found', 'RecordNotFound')
|
||||
}
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: await services.moderation(db).views.recordDetail(result, {
|
||||
includeEmails: access.moderator,
|
||||
}),
|
||||
body: recordDetail,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
@ -1,23 +1,54 @@
|
||||
import { InvalidRequestError } from '@atproto/xrpc-server'
|
||||
import { Server } from '../../../../lexicon'
|
||||
import AppContext from '../../../../context'
|
||||
import { authPassthru, mergeRepoViewPdsDetails } from './util'
|
||||
|
||||
export default function (server: Server, ctx: AppContext) {
|
||||
server.com.atproto.admin.getRepo({
|
||||
auth: ctx.roleVerifier,
|
||||
handler: async ({ params, auth }) => {
|
||||
handler: async ({ req, params, auth }) => {
|
||||
const access = auth.credentials
|
||||
const { db, services } = ctx
|
||||
const { did } = params
|
||||
const result = await services.account(db).getAccount(did, true)
|
||||
if (!result) {
|
||||
const repoDetail =
|
||||
result &&
|
||||
(await services.moderation(db).views.repoDetail(result, {
|
||||
includeEmails: access.moderator,
|
||||
}))
|
||||
|
||||
if (ctx.shouldProxyModeration()) {
|
||||
try {
|
||||
let { data: repoDetailAppview } =
|
||||
await ctx.appviewAgent.com.atproto.admin.getRepo(
|
||||
params,
|
||||
authPassthru(req),
|
||||
)
|
||||
if (repoDetail) {
|
||||
repoDetailAppview = mergeRepoViewPdsDetails(
|
||||
repoDetailAppview,
|
||||
repoDetail,
|
||||
)
|
||||
}
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: repoDetailAppview,
|
||||
}
|
||||
} catch (err) {
|
||||
if (err && err['error'] === 'RepoNotFound') {
|
||||
throw new InvalidRequestError('Repo not found', 'RepoNotFound')
|
||||
} else {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!repoDetail) {
|
||||
throw new InvalidRequestError('Repo not found', 'RepoNotFound')
|
||||
}
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: await services.moderation(db).views.repoDetail(result, {
|
||||
includeEmails: access.moderator,
|
||||
}),
|
||||
body: repoDetail,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
@ -1,10 +1,23 @@
|
||||
import { Server } from '../../../../lexicon'
|
||||
import AppContext from '../../../../context'
|
||||
import { authPassthru } from './util'
|
||||
|
||||
export default function (server: Server, ctx: AppContext) {
|
||||
server.com.atproto.admin.resolveModerationReports({
|
||||
auth: ctx.roleVerifier,
|
||||
handler: async ({ input }) => {
|
||||
handler: async ({ req, input }) => {
|
||||
if (ctx.shouldProxyModeration()) {
|
||||
const { data: result } =
|
||||
await ctx.appviewAgent.com.atproto.admin.resolveModerationReports(
|
||||
input.body,
|
||||
authPassthru(req, true),
|
||||
)
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: result,
|
||||
}
|
||||
}
|
||||
|
||||
const { db, services } = ctx
|
||||
const moderationService = services.moderation(db)
|
||||
const { actionId, reportIds, createdBy } = input.body
|
||||
|
@ -3,17 +3,70 @@ import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'
|
||||
import { Server } from '../../../../lexicon'
|
||||
import AppContext from '../../../../context'
|
||||
import {
|
||||
isRepoRef,
|
||||
ACKNOWLEDGE,
|
||||
ESCALATE,
|
||||
TAKEDOWN,
|
||||
} from '../../../../lexicon/types/com/atproto/admin/defs'
|
||||
import { isMain as isStrongRef } from '../../../../lexicon/types/com/atproto/repo/strongRef'
|
||||
import { authPassthru } from './util'
|
||||
|
||||
export default function (server: Server, ctx: AppContext) {
|
||||
server.com.atproto.admin.reverseModerationAction({
|
||||
auth: ctx.roleVerifier,
|
||||
handler: async ({ input, auth }) => {
|
||||
handler: async ({ req, input, auth }) => {
|
||||
const access = auth.credentials
|
||||
const { db, services } = ctx
|
||||
if (ctx.shouldProxyModeration()) {
|
||||
const { data: result } =
|
||||
await ctx.appviewAgent.com.atproto.admin.reverseModerationAction(
|
||||
input.body,
|
||||
authPassthru(req, true),
|
||||
)
|
||||
|
||||
const transact = db.transaction(async (dbTxn) => {
|
||||
const moderationTxn = services.moderation(dbTxn)
|
||||
const labelTxn = services.appView.label(dbTxn)
|
||||
// reverse takedowns
|
||||
if (result.action === TAKEDOWN && isRepoRef(result.subject)) {
|
||||
await moderationTxn.reverseTakedownRepo({
|
||||
did: result.subject.did,
|
||||
})
|
||||
}
|
||||
if (result.action === TAKEDOWN && isStrongRef(result.subject)) {
|
||||
await moderationTxn.reverseTakedownRecord({
|
||||
uri: new AtUri(result.subject.uri),
|
||||
})
|
||||
}
|
||||
// invert label creation & negations
|
||||
const reverseLabels = (uri: string, cid: string | null) =>
|
||||
labelTxn.formatAndCreate(ctx.cfg.labelerDid, uri, cid, {
|
||||
create: result.negateLabelVals,
|
||||
negate: result.createLabelVals,
|
||||
})
|
||||
if (isRepoRef(result.subject)) {
|
||||
await reverseLabels(result.subject.did, null)
|
||||
}
|
||||
if (isStrongRef(result.subject)) {
|
||||
await reverseLabels(result.subject.uri, result.subject.cid)
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
await transact
|
||||
} catch (err) {
|
||||
req.log.error(
|
||||
{ err, actionId: input.body.id },
|
||||
'proxied moderation action reversal failed',
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: result,
|
||||
}
|
||||
}
|
||||
|
||||
const moderationService = services.moderation(db)
|
||||
const { id, createdBy, reason } = input.body
|
||||
|
||||
|
@ -3,11 +3,26 @@ import AppContext from '../../../../context'
|
||||
import { SearchKeyset } from '../../../../services/util/search'
|
||||
import { sql } from 'kysely'
|
||||
import { ListKeyset } from '../../../../services/account'
|
||||
import { authPassthru } from './util'
|
||||
|
||||
export default function (server: Server, ctx: AppContext) {
|
||||
server.com.atproto.admin.searchRepos({
|
||||
auth: ctx.roleVerifier,
|
||||
handler: async ({ params, auth }) => {
|
||||
handler: async ({ req, params, auth }) => {
|
||||
if (ctx.shouldProxyModeration()) {
|
||||
// @TODO merge invite details to this list view. could also add
|
||||
// support for invitedBy param, which is not supported by appview.
|
||||
const { data: result } =
|
||||
await ctx.appviewAgent.com.atproto.admin.searchRepos(
|
||||
params,
|
||||
authPassthru(req),
|
||||
)
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: result,
|
||||
}
|
||||
}
|
||||
|
||||
const access = auth.credentials
|
||||
const { db, services } = ctx
|
||||
const moderationService = services.moderation(db)
|
||||
|
@ -1,21 +1,79 @@
|
||||
import { CID } from 'multiformats/cid'
|
||||
import { AtUri } from '@atproto/uri'
|
||||
import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'
|
||||
import { Server } from '../../../../lexicon'
|
||||
import AppContext from '../../../../context'
|
||||
import {
|
||||
isRepoRef,
|
||||
ACKNOWLEDGE,
|
||||
ESCALATE,
|
||||
TAKEDOWN,
|
||||
} from '../../../../lexicon/types/com/atproto/admin/defs'
|
||||
import { isMain as isStrongRef } from '../../../../lexicon/types/com/atproto/repo/strongRef'
|
||||
import { getSubject, getAction } from '../moderation/util'
|
||||
import { AuthRequiredError, InvalidRequestError } from '@atproto/xrpc-server'
|
||||
import { authPassthru } from './util'
|
||||
|
||||
export default function (server: Server, ctx: AppContext) {
|
||||
server.com.atproto.admin.takeModerationAction({
|
||||
auth: ctx.roleVerifier,
|
||||
handler: async ({ input, auth }) => {
|
||||
handler: async ({ req, input, auth }) => {
|
||||
const access = auth.credentials
|
||||
const { db, services } = ctx
|
||||
if (ctx.shouldProxyModeration()) {
|
||||
const { data: result } =
|
||||
await ctx.appviewAgent.com.atproto.admin.takeModerationAction(
|
||||
input.body,
|
||||
authPassthru(req, true),
|
||||
)
|
||||
|
||||
const transact = db.transaction(async (dbTxn) => {
|
||||
const authTxn = services.auth(dbTxn)
|
||||
const moderationTxn = services.moderation(dbTxn)
|
||||
const labelTxn = services.appView.label(dbTxn)
|
||||
// perform takedowns
|
||||
if (result.action === TAKEDOWN && isRepoRef(result.subject)) {
|
||||
await authTxn.revokeRefreshTokensByDid(result.subject.did)
|
||||
await moderationTxn.takedownRepo({
|
||||
takedownId: result.id,
|
||||
did: result.subject.did,
|
||||
})
|
||||
}
|
||||
if (result.action === TAKEDOWN && isStrongRef(result.subject)) {
|
||||
await moderationTxn.takedownRecord({
|
||||
takedownId: result.id,
|
||||
uri: new AtUri(result.subject.uri),
|
||||
blobCids: result.subjectBlobCids.map((cid) => CID.parse(cid)),
|
||||
})
|
||||
}
|
||||
// apply label creation & negations
|
||||
const applyLabels = (uri: string, cid: string | null) =>
|
||||
labelTxn.formatAndCreate(ctx.cfg.labelerDid, uri, cid, {
|
||||
create: result.createLabelVals,
|
||||
negate: result.negateLabelVals,
|
||||
})
|
||||
if (isRepoRef(result.subject)) {
|
||||
await applyLabels(result.subject.did, null)
|
||||
}
|
||||
if (isStrongRef(result.subject)) {
|
||||
await applyLabels(result.subject.uri, result.subject.cid)
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
await transact
|
||||
} catch (err) {
|
||||
req.log.error(
|
||||
{ err, actionId: result.id },
|
||||
'proxied moderation action failed',
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: result,
|
||||
}
|
||||
}
|
||||
|
||||
const moderationService = services.moderation(db)
|
||||
const {
|
||||
action,
|
||||
|
41
packages/pds/src/api/com/atproto/admin/util.ts
Normal file
41
packages/pds/src/api/com/atproto/admin/util.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import express from 'express'
|
||||
import {
|
||||
RepoView,
|
||||
RepoViewDetail,
|
||||
} from '../../../../lexicon/types/com/atproto/admin/defs'
|
||||
|
||||
// Output designed to passed as second arg to AtpAgent methods.
|
||||
// The encoding field here is a quirk of the AtpAgent.
|
||||
export function authPassthru(
|
||||
req: express.Request,
|
||||
withEncoding?: false,
|
||||
): { headers: { authorization: string }; encoding: undefined } | undefined
|
||||
|
||||
export function authPassthru(
|
||||
req: express.Request,
|
||||
withEncoding: true,
|
||||
):
|
||||
| { headers: { authorization: string }; encoding: 'application/json' }
|
||||
| undefined
|
||||
|
||||
export function authPassthru(req: express.Request, withEncoding?: boolean) {
|
||||
if (req.headers.authorization) {
|
||||
return {
|
||||
headers: { authorization: req.headers.authorization },
|
||||
encoding: withEncoding ? 'application/json' : undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @NOTE mutates.
|
||||
// merges-in details that the pds knows about the repo.
|
||||
export function mergeRepoViewPdsDetails<T extends RepoView | RepoViewDetail>(
|
||||
other: T,
|
||||
pds: T,
|
||||
) {
|
||||
other.email ??= pds.email
|
||||
other.invites ??= pds.invites
|
||||
other.invitedBy ??= pds.invitedBy
|
||||
other.invitesDisabled ??= pds.invitesDisabled
|
||||
return other
|
||||
}
|
@ -6,9 +6,25 @@ export default function (server: Server, ctx: AppContext) {
|
||||
server.com.atproto.moderation.createReport({
|
||||
auth: ctx.accessVerifierCheckTakedown,
|
||||
handler: async ({ input, auth }) => {
|
||||
const requester = auth.credentials.did
|
||||
|
||||
if (ctx.shouldProxyModeration()) {
|
||||
const { data: result } =
|
||||
await ctx.appviewAgent.com.atproto.moderation.createReport(
|
||||
input.body,
|
||||
{
|
||||
...(await ctx.serviceAuthHeaders(requester)),
|
||||
encoding: 'application/json',
|
||||
},
|
||||
)
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
body: result,
|
||||
}
|
||||
}
|
||||
|
||||
const { db, services } = ctx
|
||||
const { reasonType, reason, subject } = input.body
|
||||
const requester = auth.credentials.did
|
||||
|
||||
const moderationService = services.moderation(db)
|
||||
|
||||
|
@ -4,6 +4,7 @@ import { FeedKeyset } from '../util/feed'
|
||||
import { paginate } from '../../../../../db/pagination'
|
||||
import AppContext from '../../../../../context'
|
||||
import { FeedRow } from '../../../../services/feed'
|
||||
import { authPassthru } from '../../../../../api/com/atproto/admin/util'
|
||||
|
||||
export default function (server: Server, ctx: AppContext) {
|
||||
server.app.bsky.feed.getAuthorFeed({
|
||||
@ -16,12 +17,7 @@ export default function (server: Server, ctx: AppContext) {
|
||||
params,
|
||||
requester
|
||||
? await ctx.serviceAuthHeaders(requester)
|
||||
: {
|
||||
// @TODO use authPassthru() once it lands
|
||||
headers: req.headers.authorization
|
||||
? { authorization: req.headers.authorization }
|
||||
: {},
|
||||
},
|
||||
: authPassthru(req),
|
||||
)
|
||||
return {
|
||||
encoding: 'application/json',
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as common from '@atproto/common'
|
||||
import { Server } from '../../../../../lexicon'
|
||||
import AppContext from '../../../../../context'
|
||||
import { PostView } from '@atproto/api/src/client/types/app/bsky/feed/defs'
|
||||
import { PostView } from '../../../../../lexicon/types/app/bsky/feed/defs'
|
||||
|
||||
export default function (server: Server, ctx: AppContext) {
|
||||
server.app.bsky.feed.getPosts({
|
||||
|
@ -66,6 +66,7 @@ export interface ServerConfigValues {
|
||||
dbTxLockNonce?: string
|
||||
|
||||
bskyAppViewEndpoint?: string
|
||||
bskyAppViewModeration?: boolean
|
||||
bskyAppViewDid?: string
|
||||
bskyAppViewProxy: boolean
|
||||
|
||||
@ -211,6 +212,8 @@ export class ServerConfig {
|
||||
const bskyAppViewEndpoint = nonemptyString(
|
||||
process.env.BSKY_APP_VIEW_ENDPOINT,
|
||||
)
|
||||
const bskyAppViewModeration =
|
||||
process.env.BSKY_APP_VIEW_MODERATION === 'true' ? true : false
|
||||
const bskyAppViewDid = nonemptyString(process.env.BSKY_APP_VIEW_DID)
|
||||
const bskyAppViewProxy =
|
||||
process.env.BSKY_APP_VIEW_PROXY === 'true' ? true : false
|
||||
@ -268,6 +271,7 @@ export class ServerConfig {
|
||||
sequencerLeaderEnabled,
|
||||
dbTxLockNonce,
|
||||
bskyAppViewEndpoint,
|
||||
bskyAppViewModeration,
|
||||
bskyAppViewDid,
|
||||
bskyAppViewProxy,
|
||||
crawlersToNotify,
|
||||
@ -497,6 +501,10 @@ export class ServerConfig {
|
||||
return this.cfg.bskyAppViewEndpoint
|
||||
}
|
||||
|
||||
get bskyAppViewModeration() {
|
||||
return this.cfg.bskyAppViewModeration
|
||||
}
|
||||
|
||||
get bskyAppViewDid() {
|
||||
return this.cfg.bskyAppViewDid
|
||||
}
|
||||
|
@ -207,6 +207,13 @@ export class AppContext {
|
||||
)
|
||||
}
|
||||
|
||||
shouldProxyModeration(): boolean {
|
||||
return (
|
||||
this.cfg.bskyAppViewEndpoint !== undefined &&
|
||||
this.cfg.bskyAppViewModeration === true
|
||||
)
|
||||
}
|
||||
|
||||
canProxyWrite(): boolean {
|
||||
return (
|
||||
this.cfg.bskyAppViewEndpoint !== undefined &&
|
||||
|
@ -0,0 +1,56 @@
|
||||
import { Kysely } from 'kysely'
|
||||
import { Dialect } from '..'
|
||||
|
||||
export async function up(db: Kysely<unknown>, dialect: Dialect): Promise<void> {
|
||||
if (dialect === 'sqlite') {
|
||||
return
|
||||
}
|
||||
await db.schema
|
||||
.alterTable('repo_root')
|
||||
.dropConstraint('repo_root_takedown_id_fkey')
|
||||
.execute()
|
||||
await db.schema
|
||||
.alterTable('record')
|
||||
.dropConstraint('record_takedown_id_fkey')
|
||||
.execute()
|
||||
await db.schema
|
||||
.alterTable('repo_blob')
|
||||
.dropConstraint('repo_blob_takedown_id_fkey')
|
||||
.execute()
|
||||
}
|
||||
|
||||
export async function down(
|
||||
db: Kysely<unknown>,
|
||||
dialect: Dialect,
|
||||
): Promise<void> {
|
||||
if (dialect === 'sqlite') {
|
||||
return
|
||||
}
|
||||
await db.schema
|
||||
.alterTable('repo_root')
|
||||
.addForeignKeyConstraint(
|
||||
'repo_root_takedown_id_fkey',
|
||||
['takedownId'],
|
||||
'moderation_action',
|
||||
['id'],
|
||||
)
|
||||
.execute()
|
||||
await db.schema
|
||||
.alterTable('record')
|
||||
.addForeignKeyConstraint(
|
||||
'record_takedown_id_fkey',
|
||||
['takedownId'],
|
||||
'moderation_action',
|
||||
['id'],
|
||||
)
|
||||
.execute()
|
||||
await db.schema
|
||||
.alterTable('repo_blob')
|
||||
.addForeignKeyConstraint(
|
||||
'repo_blob_takedown_id_fkey',
|
||||
['takedownId'],
|
||||
'moderation_action',
|
||||
['id'],
|
||||
)
|
||||
.execute()
|
||||
}
|
@ -57,3 +57,4 @@ export * as _20230703T044601833Z from './20230703T044601833Z-feed-and-label-indi
|
||||
export * as _20230718T170914772Z from './20230718T170914772Z-sequencer-leader-sequence'
|
||||
export * as _20230727T172043676Z from './20230727T172043676Z-user-account-cursor-idx'
|
||||
export * as _20230801T141349990Z from './20230801T141349990Z-invite-note'
|
||||
export * as _20230801T195109532Z from './20230801T195109532Z-remove-moderation-fkeys'
|
||||
|
@ -17,7 +17,7 @@ describe('unspecced.applyLabels', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
server = await runTestServer({
|
||||
dbPostgresSchema: 'moderation',
|
||||
dbPostgresSchema: 'apply_labels',
|
||||
})
|
||||
close = server.close
|
||||
agent = new AtpAgent({ service: server.url })
|
||||
|
@ -26,7 +26,7 @@ describe('labeler', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
server = await runTestServer({
|
||||
dbPostgresSchema: 'views_author_feed',
|
||||
dbPostgresSchema: 'labeler',
|
||||
})
|
||||
close = server.close
|
||||
ctx = server.ctx
|
||||
|
428
packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap
Normal file
428
packages/pds/tests/proxied/__snapshots__/admin.test.ts.snap
Normal file
@ -0,0 +1,428 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`proxies admin requests creates reports of a repo. 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"createdAt": "1970-01-01T00:00:00.000Z",
|
||||
"id": 1,
|
||||
"reasonType": "com.atproto.moderation.defs#reasonSpam",
|
||||
"reportedBy": "user(0)",
|
||||
"subject": Object {
|
||||
"$type": "com.atproto.admin.defs#repoRef",
|
||||
"did": "user(1)",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"createdAt": "1970-01-01T00:00:00.000Z",
|
||||
"id": 2,
|
||||
"reason": "impersonation",
|
||||
"reasonType": "com.atproto.moderation.defs#reasonOther",
|
||||
"reportedBy": "user(2)",
|
||||
"subject": Object {
|
||||
"$type": "com.atproto.admin.defs#repoRef",
|
||||
"did": "user(1)",
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`proxies admin requests fetches a list of actions. 1`] = `
|
||||
Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"action": "com.atproto.admin.defs#acknowledge",
|
||||
"createdAt": "1970-01-01T00:00:00.000Z",
|
||||
"createdBy": "did:example:admin",
|
||||
"id": 3,
|
||||
"reason": "Y",
|
||||
"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 [],
|
||||
},
|
||||
Object {
|
||||
"action": "com.atproto.admin.defs#flag",
|
||||
"createdAt": "1970-01-01T00:00:00.000Z",
|
||||
"createdBy": "did:example:admin",
|
||||
"id": 2,
|
||||
"reason": "Y",
|
||||
"resolvedReportIds": Array [
|
||||
2,
|
||||
1,
|
||||
],
|
||||
"subject": Object {
|
||||
"$type": "com.atproto.repo.strongRef",
|
||||
"cid": "cids(0)",
|
||||
"uri": "record(0)",
|
||||
},
|
||||
"subjectBlobCids": Array [],
|
||||
},
|
||||
],
|
||||
"cursor": "2",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`proxies admin requests fetches a list of reports. 1`] = `
|
||||
Object {
|
||||
"cursor": "2",
|
||||
"reports": Array [
|
||||
Object {
|
||||
"createdAt": "1970-01-01T00:00:00.000Z",
|
||||
"id": 1,
|
||||
"reasonType": "com.atproto.moderation.defs#reasonSpam",
|
||||
"reportedBy": "user(0)",
|
||||
"resolvedByActionIds": Array [
|
||||
2,
|
||||
],
|
||||
"subject": Object {
|
||||
"$type": "com.atproto.admin.defs#repoRef",
|
||||
"did": "user(1)",
|
||||
},
|
||||
"subjectRepoHandle": "bob.test",
|
||||
},
|
||||
Object {
|
||||
"createdAt": "1970-01-01T00:00:00.000Z",
|
||||
"id": 2,
|
||||
"reason": "impersonation",
|
||||
"reasonType": "com.atproto.moderation.defs#reasonOther",
|
||||
"reportedBy": "user(2)",
|
||||
"resolvedByActionIds": Array [
|
||||
2,
|
||||
],
|
||||
"subject": Object {
|
||||
"$type": "com.atproto.admin.defs#repoRef",
|
||||
"did": "user(1)",
|
||||
},
|
||||
"subjectRepoHandle": "bob.test",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`proxies admin requests fetches action details. 1`] = `
|
||||
Object {
|
||||
"action": "com.atproto.admin.defs#acknowledge",
|
||||
"createdAt": "1970-01-01T00:00:00.000Z",
|
||||
"createdBy": "did:example:admin",
|
||||
"id": 3,
|
||||
"reason": "Y",
|
||||
"resolvedReports": Array [],
|
||||
"reversal": Object {
|
||||
"createdAt": "1970-01-01T00:00:00.000Z",
|
||||
"createdBy": "did:example:admin",
|
||||
"reason": "X",
|
||||
},
|
||||
"subject": Object {
|
||||
"$type": "com.atproto.admin.defs#repoView",
|
||||
"did": "user(0)",
|
||||
"handle": "bob.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(0)",
|
||||
},
|
||||
"size": 3976,
|
||||
},
|
||||
"description": "hi im bob label_me",
|
||||
"displayName": "bobby",
|
||||
},
|
||||
],
|
||||
},
|
||||
"subjectBlobs": Array [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`proxies admin requests fetches record details. 1`] = `
|
||||
Object {
|
||||
"blobCids": Array [],
|
||||
"blobs": Array [],
|
||||
"cid": "cids(0)",
|
||||
"indexedAt": "1970-01-01T00:00:00.000Z",
|
||||
"labels": Array [],
|
||||
"moderation": Object {
|
||||
"actions": Array [
|
||||
Object {
|
||||
"action": "com.atproto.admin.defs#flag",
|
||||
"createdAt": "1970-01-01T00:00:00.000Z",
|
||||
"createdBy": "did:example:admin",
|
||||
"id": 2,
|
||||
"reason": "Y",
|
||||
"resolvedReportIds": Array [
|
||||
2,
|
||||
1,
|
||||
],
|
||||
"subject": Object {
|
||||
"$type": "com.atproto.repo.strongRef",
|
||||
"cid": "cids(0)",
|
||||
"uri": "record(0)",
|
||||
},
|
||||
"subjectBlobCids": Array [],
|
||||
},
|
||||
],
|
||||
"currentAction": Object {
|
||||
"action": "com.atproto.admin.defs#flag",
|
||||
"id": 2,
|
||||
},
|
||||
"reports": Array [],
|
||||
},
|
||||
"repo": Object {
|
||||
"did": "user(0)",
|
||||
"email": "bob@test.com",
|
||||
"handle": "bob.test",
|
||||
"indexedAt": "1970-01-01T00:00:00.000Z",
|
||||
"invitedBy": Object {
|
||||
"available": 10,
|
||||
"code": "invite-code",
|
||||
"createdAt": "1970-01-01T00:00:00.000Z",
|
||||
"createdBy": "admin",
|
||||
"disabled": false,
|
||||
"forAccount": "admin",
|
||||
"uses": Array [
|
||||
Object {
|
||||
"usedAt": "1970-01-01T00:00:00.000Z",
|
||||
"usedBy": "user(1)",
|
||||
},
|
||||
Object {
|
||||
"usedAt": "1970-01-01T00:00:00.000Z",
|
||||
"usedBy": "user(0)",
|
||||
},
|
||||
Object {
|
||||
"usedAt": "1970-01-01T00:00:00.000Z",
|
||||
"usedBy": "user(2)",
|
||||
},
|
||||
Object {
|
||||
"usedAt": "1970-01-01T00:00:00.000Z",
|
||||
"usedBy": "user(3)",
|
||||
},
|
||||
],
|
||||
},
|
||||
"invitesDisabled": true,
|
||||
"moderation": Object {
|
||||
"currentAction": Object {
|
||||
"action": "com.atproto.admin.defs#acknowledge",
|
||||
"id": 3,
|
||||
},
|
||||
},
|
||||
"relatedRecords": Array [
|
||||
Object {
|
||||
"$type": "app.bsky.actor.profile",
|
||||
"avatar": Object {
|
||||
"$type": "blob",
|
||||
"mimeType": "image/jpeg",
|
||||
"ref": Object {
|
||||
"$link": "cids(1)",
|
||||
},
|
||||
"size": 3976,
|
||||
},
|
||||
"description": "hi im bob label_me",
|
||||
"displayName": "bobby",
|
||||
},
|
||||
],
|
||||
},
|
||||
"uri": "record(0)",
|
||||
"value": Object {
|
||||
"$type": "app.bsky.feed.post",
|
||||
"createdAt": "1970-01-01T00:00:00.000+00:00",
|
||||
"text": "bobby boy here",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`proxies admin requests fetches repo details. 1`] = `
|
||||
Object {
|
||||
"did": "user(0)",
|
||||
"email": "eve@test.com",
|
||||
"handle": "eve.test",
|
||||
"indexedAt": "1970-01-01T00:00:00.000Z",
|
||||
"invitedBy": Object {
|
||||
"available": 1,
|
||||
"code": "invite-code",
|
||||
"createdAt": "1970-01-01T00:00:00.000Z",
|
||||
"createdBy": "admin",
|
||||
"disabled": false,
|
||||
"forAccount": "user(1)",
|
||||
"uses": Array [
|
||||
Object {
|
||||
"usedAt": "1970-01-01T00:00:00.000Z",
|
||||
"usedBy": "user(0)",
|
||||
},
|
||||
],
|
||||
},
|
||||
"invites": Array [],
|
||||
"invitesDisabled": false,
|
||||
"labels": Array [],
|
||||
"moderation": Object {
|
||||
"actions": Array [],
|
||||
"reports": Array [],
|
||||
},
|
||||
"relatedRecords": Array [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`proxies admin requests fetches report details. 1`] = `
|
||||
Object {
|
||||
"createdAt": "1970-01-01T00:00:00.000Z",
|
||||
"id": 1,
|
||||
"reasonType": "com.atproto.moderation.defs#reasonSpam",
|
||||
"reportedBy": "user(0)",
|
||||
"resolvedByActions": Array [
|
||||
Object {
|
||||
"action": "com.atproto.admin.defs#flag",
|
||||
"createdAt": "1970-01-01T00:00:00.000Z",
|
||||
"createdBy": "did:example:admin",
|
||||
"id": 2,
|
||||
"reason": "Y",
|
||||
"resolvedReportIds": Array [
|
||||
2,
|
||||
1,
|
||||
],
|
||||
"subject": Object {
|
||||
"$type": "com.atproto.repo.strongRef",
|
||||
"cid": "cids(1)",
|
||||
"uri": "record(0)",
|
||||
},
|
||||
"subjectBlobCids": Array [],
|
||||
},
|
||||
],
|
||||
"subject": Object {
|
||||
"$type": "com.atproto.admin.defs#repoView",
|
||||
"did": "user(1)",
|
||||
"handle": "bob.test",
|
||||
"indexedAt": "1970-01-01T00:00:00.000Z",
|
||||
"moderation": Object {
|
||||
"currentAction": Object {
|
||||
"action": "com.atproto.admin.defs#acknowledge",
|
||||
"id": 3,
|
||||
},
|
||||
},
|
||||
"relatedRecords": Array [
|
||||
Object {
|
||||
"$type": "app.bsky.actor.profile",
|
||||
"avatar": Object {
|
||||
"$type": "blob",
|
||||
"mimeType": "image/jpeg",
|
||||
"ref": Object {
|
||||
"$link": "cids(0)",
|
||||
},
|
||||
"size": 3976,
|
||||
},
|
||||
"description": "hi im bob label_me",
|
||||
"displayName": "bobby",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`proxies admin requests reverses action. 1`] = `
|
||||
Object {
|
||||
"action": "com.atproto.admin.defs#acknowledge",
|
||||
"createdAt": "1970-01-01T00:00:00.000Z",
|
||||
"createdBy": "did:example:admin",
|
||||
"id": 3,
|
||||
"reason": "Y",
|
||||
"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 [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`proxies admin requests searches repos. 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"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(0)",
|
||||
},
|
||||
"size": 3976,
|
||||
},
|
||||
"description": "its me!",
|
||||
"displayName": "ali",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`proxies admin requests takes actions and resolves reports 1`] = `
|
||||
Object {
|
||||
"action": "com.atproto.admin.defs#flag",
|
||||
"createdAt": "1970-01-01T00:00:00.000Z",
|
||||
"createdBy": "did:example:admin",
|
||||
"id": 2,
|
||||
"reason": "Y",
|
||||
"resolvedReportIds": Array [],
|
||||
"subject": Object {
|
||||
"$type": "com.atproto.repo.strongRef",
|
||||
"cid": "cids(0)",
|
||||
"uri": "record(0)",
|
||||
},
|
||||
"subjectBlobCids": Array [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`proxies admin requests takes actions and resolves reports 2`] = `
|
||||
Object {
|
||||
"action": "com.atproto.admin.defs#acknowledge",
|
||||
"createdAt": "1970-01-01T00:00:00.000Z",
|
||||
"createdBy": "did:example:admin",
|
||||
"id": 3,
|
||||
"reason": "Y",
|
||||
"resolvedReportIds": Array [],
|
||||
"subject": Object {
|
||||
"$type": "com.atproto.admin.defs#repoRef",
|
||||
"did": "user(0)",
|
||||
},
|
||||
"subjectBlobCids": Array [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`proxies admin requests takes actions and resolves reports 3`] = `
|
||||
Object {
|
||||
"action": "com.atproto.admin.defs#flag",
|
||||
"createdAt": "1970-01-01T00:00:00.000Z",
|
||||
"createdBy": "did:example:admin",
|
||||
"id": 2,
|
||||
"reason": "Y",
|
||||
"resolvedReportIds": Array [
|
||||
2,
|
||||
1,
|
||||
],
|
||||
"subject": Object {
|
||||
"$type": "com.atproto.repo.strongRef",
|
||||
"cid": "cids(0)",
|
||||
"uri": "record(0)",
|
||||
},
|
||||
"subjectBlobCids": Array [],
|
||||
}
|
||||
`;
|
405
packages/pds/tests/proxied/admin.test.ts
Normal file
405
packages/pds/tests/proxied/admin.test.ts
Normal file
@ -0,0 +1,405 @@
|
||||
import AtpAgent from '@atproto/api'
|
||||
import { TestNetwork } from '@atproto/dev-env'
|
||||
import { SeedClient } from '../seeds/client'
|
||||
import basicSeed from '../seeds/basic'
|
||||
import {
|
||||
REASONOTHER,
|
||||
REASONSPAM,
|
||||
} from '@atproto/api/src/client/types/com/atproto/moderation/defs'
|
||||
import { forSnapshot } from '../_util'
|
||||
import {
|
||||
ACKNOWLEDGE,
|
||||
FLAG,
|
||||
TAKEDOWN,
|
||||
} from '@atproto/api/src/client/types/com/atproto/admin/defs'
|
||||
import { NotFoundError } from '@atproto/api/src/client/types/app/bsky/feed/getPostThread'
|
||||
|
||||
describe('proxies admin requests', () => {
|
||||
let network: TestNetwork
|
||||
let agent: AtpAgent
|
||||
let sc: SeedClient
|
||||
|
||||
beforeAll(async () => {
|
||||
network = await TestNetwork.create({
|
||||
dbPostgresSchema: 'proxy_admin',
|
||||
pds: {
|
||||
// @NOTE requires admin pass be the same on pds and appview, which TestNetwork is handling for us.
|
||||
enableInProcessAppView: true,
|
||||
bskyAppViewModeration: true,
|
||||
inviteRequired: true,
|
||||
},
|
||||
})
|
||||
agent = network.pds.getClient()
|
||||
sc = new SeedClient(agent)
|
||||
const { data: invite } =
|
||||
await agent.api.com.atproto.server.createInviteCode(
|
||||
{ useCount: 10 },
|
||||
{
|
||||
encoding: 'application/json',
|
||||
headers: network.pds.adminAuthHeaders(),
|
||||
},
|
||||
)
|
||||
await basicSeed(sc, invite)
|
||||
await network.processAll()
|
||||
})
|
||||
|
||||
beforeAll(async () => {
|
||||
const { data: invite } =
|
||||
await agent.api.com.atproto.server.createInviteCode(
|
||||
{ useCount: 1, forAccount: sc.dids.alice },
|
||||
{
|
||||
headers: network.pds.adminAuthHeaders(),
|
||||
encoding: 'application/json',
|
||||
},
|
||||
)
|
||||
await agent.api.com.atproto.admin.disableAccountInvites(
|
||||
{ account: sc.dids.bob },
|
||||
{ headers: network.pds.adminAuthHeaders(), encoding: 'application/json' },
|
||||
)
|
||||
await sc.createAccount('eve', {
|
||||
handle: 'eve.test',
|
||||
email: 'eve@test.com',
|
||||
password: 'password',
|
||||
inviteCode: invite.code,
|
||||
})
|
||||
await network.processAll()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await network.close()
|
||||
})
|
||||
|
||||
it('creates reports of a repo.', async () => {
|
||||
const { data: reportA } =
|
||||
await agent.api.com.atproto.moderation.createReport(
|
||||
{
|
||||
reasonType: REASONSPAM,
|
||||
subject: {
|
||||
$type: 'com.atproto.admin.defs#repoRef',
|
||||
did: sc.dids.bob,
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: sc.getHeaders(sc.dids.alice),
|
||||
encoding: 'application/json',
|
||||
},
|
||||
)
|
||||
const { data: reportB } =
|
||||
await agent.api.com.atproto.moderation.createReport(
|
||||
{
|
||||
reasonType: REASONOTHER,
|
||||
reason: 'impersonation',
|
||||
subject: {
|
||||
$type: 'com.atproto.admin.defs#repoRef',
|
||||
did: sc.dids.bob,
|
||||
},
|
||||
},
|
||||
{
|
||||
headers: sc.getHeaders(sc.dids.carol),
|
||||
encoding: 'application/json',
|
||||
},
|
||||
)
|
||||
expect(forSnapshot([reportA, reportB])).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('takes actions and resolves reports', async () => {
|
||||
const post = sc.posts[sc.dids.bob][1]
|
||||
const { data: actionA } =
|
||||
await agent.api.com.atproto.admin.takeModerationAction(
|
||||
{
|
||||
action: FLAG,
|
||||
subject: {
|
||||
$type: 'com.atproto.repo.strongRef',
|
||||
uri: post.ref.uriStr,
|
||||
cid: post.ref.cidStr,
|
||||
},
|
||||
createdBy: 'did:example:admin',
|
||||
reason: 'Y',
|
||||
},
|
||||
{
|
||||
headers: network.pds.adminAuthHeaders(),
|
||||
encoding: 'application/json',
|
||||
},
|
||||
)
|
||||
expect(forSnapshot(actionA)).toMatchSnapshot()
|
||||
const { data: actionB } =
|
||||
await agent.api.com.atproto.admin.takeModerationAction(
|
||||
{
|
||||
action: ACKNOWLEDGE,
|
||||
subject: {
|
||||
$type: 'com.atproto.admin.defs#repoRef',
|
||||
did: sc.dids.bob,
|
||||
},
|
||||
createdBy: 'did:example:admin',
|
||||
reason: 'Y',
|
||||
},
|
||||
{
|
||||
headers: network.pds.adminAuthHeaders(),
|
||||
encoding: 'application/json',
|
||||
},
|
||||
)
|
||||
expect(forSnapshot(actionB)).toMatchSnapshot()
|
||||
const { data: resolved } =
|
||||
await agent.api.com.atproto.admin.resolveModerationReports(
|
||||
{
|
||||
actionId: actionA.id,
|
||||
reportIds: [1, 2],
|
||||
createdBy: 'did:example:admin',
|
||||
},
|
||||
{
|
||||
headers: network.pds.adminAuthHeaders(),
|
||||
encoding: 'application/json',
|
||||
},
|
||||
)
|
||||
expect(forSnapshot(resolved)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('fetches report details.', async () => {
|
||||
const { data: result } =
|
||||
await agent.api.com.atproto.admin.getModerationReport(
|
||||
{ id: 1 },
|
||||
{ headers: network.pds.adminAuthHeaders() },
|
||||
)
|
||||
expect(forSnapshot(result)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('fetches a list of reports.', async () => {
|
||||
const { data: result } =
|
||||
await agent.api.com.atproto.admin.getModerationReports(
|
||||
{ reverse: true },
|
||||
{ headers: network.pds.adminAuthHeaders() },
|
||||
)
|
||||
expect(forSnapshot(result)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('fetches repo details.', async () => {
|
||||
const { data: result } = await agent.api.com.atproto.admin.getRepo(
|
||||
{ did: sc.dids.eve },
|
||||
{ headers: network.pds.adminAuthHeaders() },
|
||||
)
|
||||
expect(forSnapshot(result)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('fetches record details.', async () => {
|
||||
const post = sc.posts[sc.dids.bob][1]
|
||||
const { data: result } = await agent.api.com.atproto.admin.getRecord(
|
||||
{ uri: post.ref.uriStr },
|
||||
{ headers: network.pds.adminAuthHeaders() },
|
||||
)
|
||||
expect(forSnapshot(result)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('reverses action.', async () => {
|
||||
const { data: result } =
|
||||
await agent.api.com.atproto.admin.reverseModerationAction(
|
||||
{ id: 3, createdBy: 'did:example:admin', reason: 'X' },
|
||||
{
|
||||
headers: network.pds.adminAuthHeaders(),
|
||||
encoding: 'application/json',
|
||||
},
|
||||
)
|
||||
expect(forSnapshot(result)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('fetches action details.', async () => {
|
||||
const { data: result } =
|
||||
await agent.api.com.atproto.admin.getModerationAction(
|
||||
{ id: 3 },
|
||||
{ headers: network.pds.adminAuthHeaders() },
|
||||
)
|
||||
expect(forSnapshot(result)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('fetches a list of actions.', async () => {
|
||||
const { data: result } =
|
||||
await agent.api.com.atproto.admin.getModerationActions(
|
||||
{ subject: sc.dids.bob },
|
||||
{ headers: network.pds.adminAuthHeaders() },
|
||||
)
|
||||
expect(forSnapshot(result)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('searches repos.', async () => {
|
||||
const { data: result } = await agent.api.com.atproto.admin.searchRepos(
|
||||
{ term: 'alice' },
|
||||
{ headers: network.pds.adminAuthHeaders() },
|
||||
)
|
||||
expect(forSnapshot(result.repos)).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('passes through errors.', async () => {
|
||||
const tryGetReport = agent.api.com.atproto.admin.getModerationReport(
|
||||
{ id: 1000 },
|
||||
{ headers: network.pds.adminAuthHeaders() },
|
||||
)
|
||||
await expect(tryGetReport).rejects.toThrow('Report not found')
|
||||
const tryGetRepo = agent.api.com.atproto.admin.getRepo(
|
||||
{ did: 'did:does:not:exist' },
|
||||
{ headers: network.pds.adminAuthHeaders() },
|
||||
)
|
||||
await expect(tryGetRepo).rejects.toThrow('Repo not found')
|
||||
const tryGetRecord = agent.api.com.atproto.admin.getRecord(
|
||||
{ uri: 'at://did:does:not:exist/bad.collection.name/badrkey' },
|
||||
{ headers: network.pds.adminAuthHeaders() },
|
||||
)
|
||||
await expect(tryGetRecord).rejects.toThrow('Record not found')
|
||||
})
|
||||
|
||||
it('takesdown and labels repos, and reverts.', async () => {
|
||||
const { db, services } = network.pds.ctx
|
||||
// takedown repo
|
||||
const { data: action } =
|
||||
await agent.api.com.atproto.admin.takeModerationAction(
|
||||
{
|
||||
action: TAKEDOWN,
|
||||
subject: {
|
||||
$type: 'com.atproto.admin.defs#repoRef',
|
||||
did: sc.dids.alice,
|
||||
},
|
||||
createdBy: 'did:example:admin',
|
||||
reason: 'Y',
|
||||
createLabelVals: ['dogs'],
|
||||
negateLabelVals: ['cats'],
|
||||
},
|
||||
{
|
||||
headers: network.pds.adminAuthHeaders(),
|
||||
encoding: 'application/json',
|
||||
},
|
||||
)
|
||||
// check profile and labels
|
||||
const tryGetProfilePds = agent.api.app.bsky.actor.getProfile(
|
||||
{ actor: sc.dids.alice },
|
||||
{ headers: sc.getHeaders(sc.dids.carol) },
|
||||
)
|
||||
const tryGetProfileAppview = agent.api.app.bsky.actor.getProfile(
|
||||
{ actor: sc.dids.alice },
|
||||
{
|
||||
headers: { ...sc.getHeaders(sc.dids.carol), 'x-appview-proxy': 'true' },
|
||||
},
|
||||
)
|
||||
await expect(tryGetProfilePds).rejects.toThrow(
|
||||
'Account has been taken down',
|
||||
)
|
||||
await expect(tryGetProfileAppview).rejects.toThrow(
|
||||
'Account has been taken down',
|
||||
)
|
||||
const labelsA = await services.appView
|
||||
.label(db)
|
||||
.getLabels(sc.dids.alice, false, true)
|
||||
expect(labelsA.map((l) => l.val)).toEqual(['dogs'])
|
||||
// reverse action
|
||||
await agent.api.com.atproto.admin.reverseModerationAction(
|
||||
{ id: action.id, createdBy: 'did:example:admin', reason: 'X' },
|
||||
{
|
||||
headers: network.pds.adminAuthHeaders(),
|
||||
encoding: 'application/json',
|
||||
},
|
||||
)
|
||||
// check profile and labels
|
||||
const { data: profilePds } = await agent.api.app.bsky.actor.getProfile(
|
||||
{ actor: sc.dids.alice },
|
||||
{ headers: sc.getHeaders(sc.dids.carol) },
|
||||
)
|
||||
const { data: profileAppview } = await agent.api.app.bsky.actor.getProfile(
|
||||
{ actor: sc.dids.alice },
|
||||
{
|
||||
headers: { ...sc.getHeaders(sc.dids.carol), 'x-appview-proxy': 'true' },
|
||||
},
|
||||
)
|
||||
expect(profilePds).toEqual(
|
||||
expect.objectContaining({ did: sc.dids.alice, handle: 'alice.test' }),
|
||||
)
|
||||
expect(profileAppview).toEqual(
|
||||
expect.objectContaining({ did: sc.dids.alice, handle: 'alice.test' }),
|
||||
)
|
||||
const labelsB = await services.appView
|
||||
.label(db)
|
||||
.getLabels(sc.dids.alice, false, true)
|
||||
expect(labelsB.map((l) => l.val)).toEqual(['cats'])
|
||||
})
|
||||
|
||||
it('takesdown and labels records, and reverts.', async () => {
|
||||
const { db, services } = network.pds.ctx
|
||||
const post = sc.posts[sc.dids.alice][0]
|
||||
// takedown post
|
||||
const { data: action } =
|
||||
await agent.api.com.atproto.admin.takeModerationAction(
|
||||
{
|
||||
action: TAKEDOWN,
|
||||
subject: {
|
||||
$type: 'com.atproto.repo.strongRef',
|
||||
uri: post.ref.uriStr,
|
||||
cid: post.ref.cidStr,
|
||||
},
|
||||
createdBy: 'did:example:admin',
|
||||
reason: 'Y',
|
||||
createLabelVals: ['dogs'],
|
||||
negateLabelVals: ['cats'],
|
||||
},
|
||||
{
|
||||
headers: network.pds.adminAuthHeaders(),
|
||||
encoding: 'application/json',
|
||||
},
|
||||
)
|
||||
// check thread and labels
|
||||
const tryGetPostPds = agent.api.app.bsky.feed.getPostThread(
|
||||
{ uri: post.ref.uriStr, depth: 0 },
|
||||
{ headers: sc.getHeaders(sc.dids.carol) },
|
||||
)
|
||||
const tryGetPostAppview = agent.api.app.bsky.feed.getPostThread(
|
||||
{ uri: post.ref.uriStr, depth: 0 },
|
||||
{
|
||||
headers: { ...sc.getHeaders(sc.dids.carol), 'x-appview-proxy': 'true' },
|
||||
},
|
||||
)
|
||||
await expect(tryGetPostPds).rejects.toThrow(NotFoundError)
|
||||
await expect(tryGetPostAppview).rejects.toThrow(NotFoundError)
|
||||
const labelsA = await services.appView
|
||||
.label(db)
|
||||
.getLabels(post.ref.uriStr, false, true)
|
||||
expect(labelsA.map((l) => l.val)).toEqual(['dogs'])
|
||||
// reverse action
|
||||
await agent.api.com.atproto.admin.reverseModerationAction(
|
||||
{ id: action.id, createdBy: 'did:example:admin', reason: 'X' },
|
||||
{
|
||||
headers: network.pds.adminAuthHeaders(),
|
||||
encoding: 'application/json',
|
||||
},
|
||||
)
|
||||
// check thread and labels
|
||||
const { data: threadPds } = await agent.api.app.bsky.feed.getPostThread(
|
||||
{ uri: post.ref.uriStr, depth: 0 },
|
||||
{ headers: sc.getHeaders(sc.dids.carol) },
|
||||
)
|
||||
const { data: threadAppview } = await agent.api.app.bsky.feed.getPostThread(
|
||||
{ uri: post.ref.uriStr, depth: 0 },
|
||||
{
|
||||
headers: { ...sc.getHeaders(sc.dids.carol), 'x-appview-proxy': 'true' },
|
||||
},
|
||||
)
|
||||
expect(threadPds.thread.post).toEqual(
|
||||
expect.objectContaining({ uri: post.ref.uriStr, cid: post.ref.cidStr }),
|
||||
)
|
||||
expect(threadAppview.thread.post).toEqual(
|
||||
expect.objectContaining({ uri: post.ref.uriStr, cid: post.ref.cidStr }),
|
||||
)
|
||||
const labelsB = await services.appView
|
||||
.label(db)
|
||||
.getLabels(post.ref.uriStr, false, true)
|
||||
expect(labelsB.map((l) => l.val)).toEqual(['cats'])
|
||||
})
|
||||
|
||||
it('does not persist actions and reports on pds.', async () => {
|
||||
const { db } = network.pds.ctx
|
||||
const actions = await db.db
|
||||
.selectFrom('moderation_action')
|
||||
.selectAll()
|
||||
.execute()
|
||||
const reports = await db.db
|
||||
.selectFrom('moderation_report')
|
||||
.selectAll()
|
||||
.execute()
|
||||
expect(actions).toEqual([])
|
||||
expect(reports).toEqual([])
|
||||
})
|
||||
})
|
@ -4,8 +4,8 @@ import { adminAuth } from '../_util'
|
||||
import { SeedClient } from './client'
|
||||
import usersSeed from './users'
|
||||
|
||||
export default async (sc: SeedClient) => {
|
||||
await usersSeed(sc)
|
||||
export default async (sc: SeedClient, invite?: { code: string }) => {
|
||||
await usersSeed(sc, invite)
|
||||
|
||||
const alice = sc.dids.alice
|
||||
const bob = sc.dids.bob
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { SeedClient } from './client'
|
||||
|
||||
export default async (sc: SeedClient) => {
|
||||
await sc.createAccount('alice', users.alice)
|
||||
await sc.createAccount('bob', users.bob)
|
||||
await sc.createAccount('carol', users.carol)
|
||||
await sc.createAccount('dan', users.dan)
|
||||
export default async (sc: SeedClient, invite?: { code: string }) => {
|
||||
await sc.createAccount('alice', { ...users.alice, inviteCode: invite?.code })
|
||||
await sc.createAccount('bob', { ...users.bob, inviteCode: invite?.code })
|
||||
await sc.createAccount('carol', { ...users.carol, inviteCode: invite?.code })
|
||||
await sc.createAccount('dan', { ...users.dan, inviteCode: invite?.code })
|
||||
|
||||
await sc.createProfile(
|
||||
sc.dids.alice,
|
||||
|
Loading…
x
Reference in New Issue
Block a user