Add moderation setup

This commit is contained in:
Eric Bailey
2024-08-28 19:06:36 -05:00
parent e74bd093ee
commit d15585bea4
3 changed files with 121 additions and 30 deletions
+4
View File
@@ -9,6 +9,7 @@ import {
AppBskyFeedPost,
AppBskyGraphDefs,
AppBskyGraphStarterpack,
ModerationDecision,
} from '@atproto/api'
import {atoms as a, gradient, theme as t} from '../theme/index.js'
@@ -31,15 +32,18 @@ import {Text} from './Text.js'
export function Post({
post,
data,
moderation,
}: {
post: AppBskyFeedDefs.PostView
data: PostData
moderation: ModerationDecision
}) {
if (AppBskyFeedPost.isRecord(post.record)) {
const avatar = data.images.get(post.author.avatar)
const text = post.record.text
const rt = data.texts.get(text)
const hasInteractions = post.likeCount > 0 || post.repostCount > 0
console.log(moderation.ui('contentView'))
return (
<Box
+41 -30
View File
@@ -1,5 +1,5 @@
import React from 'react'
import {AppBskyFeedDefs, AppBskyFeedPost, AtUri} from '@atproto/api'
import {AppBskyFeedPost, AtUri} from '@atproto/api'
import resvg from '@resvg/resvg-js'
import {Express} from 'express'
import satori from 'satori'
@@ -8,6 +8,7 @@ import {Post} from '../components/Post.js'
import {AppContext} from '../context.js'
import {httpLogger} from '../logger.js'
import {loadEmojiAsSvg} from '../util.js'
import {getModerationOptions, moderatePost} from '../util/moderation.js'
import {resolvePostData} from '../util/resolvePostData.js'
import {handler, originVerifyMiddleware} from './util.js'
@@ -21,8 +22,6 @@ export default function (ctx: AppContext, app: Express) {
let {actor, rkey} = req.params
let uri = AtUri.make(actor, 'app.bsky.feed.post', rkey)
let post: AppBskyFeedDefs.PostView
try {
if (!actor.startsWith('did:')) {
const res = await ctx.appviewAgent.resolveHandle({
@@ -34,44 +33,56 @@ export default function (ctx: AppContext, app: Express) {
const {data} = await ctx.appviewAgent.getPosts({
uris: [uri.toString()],
})
post = data.posts.at(0)
const post = data.posts.at(0)
if (!AppBskyFeedPost.isRecord(post.record)) {
return res.status(404).end('not found')
}
const notPublic = post.author.labels.some(
l => l.val === `!no-unauthenticated`,
)
if (notPublic) {
return res.status(404).end('not found')
}
const [postData, moderationOptions] = await Promise.all([
resolvePostData(post, ctx.appviewAgent),
getModerationOptions(ctx.appviewAgent),
])
const svg = await satori(
<Post
post={post}
data={postData}
moderation={moderatePost(post, moderationOptions)}
/>,
{
fonts: ctx.fonts,
width: WIDTH,
loadAdditionalAsset: async (code, text) => {
if (code === 'emoji') {
return await loadEmojiAsSvg(text)
}
},
},
)
const output = await resvg.renderAsync(svg, {
fitTo: {
mode: 'width',
value: WIDTH * 2,
},
logLevel: 'trace',
})
res.statusCode = 200
res.setHeader('content-type', 'image/png')
res.setHeader('cdn-tag', [...postData.images.keys()].join(','))
return res.end(output.asPng())
} catch (err) {
httpLogger.warn({err, uri: uri.toString()}, 'could not fetch post')
return res.status(404).end('not found')
}
if (!AppBskyFeedPost.isRecord(post.record)) {
return res.status(404).end('not found')
}
const data = await resolvePostData(post, ctx.appviewAgent)
const svg = await satori(<Post post={post} data={data} />, {
fonts: ctx.fonts,
width: WIDTH,
loadAdditionalAsset: async (code, text) => {
if (code === 'emoji') {
return await loadEmojiAsSvg(text)
}
},
})
const output = await resvg.renderAsync(svg, {
fitTo: {
mode: 'width',
value: WIDTH * 2,
},
logLevel: 'trace',
})
res.statusCode = 200
res.setHeader('content-type', 'image/png')
res.setHeader('cdn-tag', [...data.images.keys()].join(','))
return res.end(output.asPng())
}),
)
}
+76
View File
@@ -0,0 +1,76 @@
/* eslint-disable-next-line no-restricted-imports */
import {
AppBskyLabelerDefs,
AtpAgent,
BSKY_LABELER_DID,
DEFAULT_LABEL_SETTINGS,
interpretLabelValueDefinitions,
moderatePost as defaultModeratePost,
} from '@atproto/api'
type ModeratePost = typeof defaultModeratePost
export type Subject = Parameters<ModeratePost>[0]
export type Options = Parameters<ModeratePost>[1]
function translateOldLabels(subject: Parameters<ModeratePost>[0]) {
if (subject.labels) {
for (const label of subject.labels) {
if (
label.val === 'gore' &&
(!label.src || label.src === BSKY_LABELER_DID)
) {
label.val = 'graphic-media'
}
}
}
}
export const DEFAULT_LABELS: typeof DEFAULT_LABEL_SETTINGS = Object.fromEntries(
Object.entries(DEFAULT_LABEL_SETTINGS).map(([key, _pref]) => [key, 'hide']),
)
export function moderatePost(post: Subject, opts: Options) {
// HACK
// temporarily translate 'gore' into 'graphic-media' during the transition period
// can remove this in a few months
// -prf
translateOldLabels(post)
return defaultModeratePost(post, opts)
}
export async function getModerationOptions(agent: AtpAgent) {
const labelDefs = await getLabelDefinitions(agent)
const opts: Options = {
userDid: undefined,
prefs: {
adultContentEnabled: false,
labels: DEFAULT_LABELS,
mutedWords: [],
hiddenPosts: [],
labelers: [
{
did: BSKY_LABELER_DID,
labels: DEFAULT_LABELS,
},
],
},
labelDefs: {
[BSKY_LABELER_DID]: labelDefs,
},
}
return opts
}
export async function getLabelDefinitions(agent: AtpAgent) {
const {data} = await agent.app.bsky.labeler.getServices({
dids: [BSKY_LABELER_DID],
detailed: true,
})
if (!data || !data.views[0]) {
throw new Error(`Could not fetch label definitions`)
}
const labeler = data.views[0] as AppBskyLabelerDefs.LabelerViewDetailed
return interpretLabelValueDefinitions(labeler)
}