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, AppBskyFeedPost,
AppBskyGraphDefs, AppBskyGraphDefs,
AppBskyGraphStarterpack, AppBskyGraphStarterpack,
ModerationDecision,
} from '@atproto/api' } from '@atproto/api'
import {atoms as a, gradient, theme as t} from '../theme/index.js' import {atoms as a, gradient, theme as t} from '../theme/index.js'
@@ -31,15 +32,18 @@ import {Text} from './Text.js'
export function Post({ export function Post({
post, post,
data, data,
moderation,
}: { }: {
post: AppBskyFeedDefs.PostView post: AppBskyFeedDefs.PostView
data: PostData data: PostData
moderation: ModerationDecision
}) { }) {
if (AppBskyFeedPost.isRecord(post.record)) { if (AppBskyFeedPost.isRecord(post.record)) {
const avatar = data.images.get(post.author.avatar) const avatar = data.images.get(post.author.avatar)
const text = post.record.text const text = post.record.text
const rt = data.texts.get(text) const rt = data.texts.get(text)
const hasInteractions = post.likeCount > 0 || post.repostCount > 0 const hasInteractions = post.likeCount > 0 || post.repostCount > 0
console.log(moderation.ui('contentView'))
return ( return (
<Box <Box
+26 -15
View File
@@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import {AppBskyFeedDefs, AppBskyFeedPost, AtUri} from '@atproto/api' import {AppBskyFeedPost, AtUri} from '@atproto/api'
import resvg from '@resvg/resvg-js' import resvg from '@resvg/resvg-js'
import {Express} from 'express' import {Express} from 'express'
import satori from 'satori' import satori from 'satori'
@@ -8,6 +8,7 @@ import {Post} from '../components/Post.js'
import {AppContext} from '../context.js' import {AppContext} from '../context.js'
import {httpLogger} from '../logger.js' import {httpLogger} from '../logger.js'
import {loadEmojiAsSvg} from '../util.js' import {loadEmojiAsSvg} from '../util.js'
import {getModerationOptions, moderatePost} from '../util/moderation.js'
import {resolvePostData} from '../util/resolvePostData.js' import {resolvePostData} from '../util/resolvePostData.js'
import {handler, originVerifyMiddleware} from './util.js' import {handler, originVerifyMiddleware} from './util.js'
@@ -21,8 +22,6 @@ export default function (ctx: AppContext, app: Express) {
let {actor, rkey} = req.params let {actor, rkey} = req.params
let uri = AtUri.make(actor, 'app.bsky.feed.post', rkey) let uri = AtUri.make(actor, 'app.bsky.feed.post', rkey)
let post: AppBskyFeedDefs.PostView
try { try {
if (!actor.startsWith('did:')) { if (!actor.startsWith('did:')) {
const res = await ctx.appviewAgent.resolveHandle({ const res = await ctx.appviewAgent.resolveHandle({
@@ -34,25 +33,30 @@ export default function (ctx: AppContext, app: Express) {
const {data} = await ctx.appviewAgent.getPosts({ const {data} = await ctx.appviewAgent.getPosts({
uris: [uri.toString()], 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( const notPublic = post.author.labels.some(
l => l.val === `!no-unauthenticated`, l => l.val === `!no-unauthenticated`,
) )
if (notPublic) { if (notPublic) {
return res.status(404).end('not found') return res.status(404).end('not found')
} }
} catch (err) {
httpLogger.warn({err, uri: uri.toString()}, 'could not fetch post')
return res.status(404).end('not found')
}
if (!AppBskyFeedPost.isRecord(post.record)) { const [postData, moderationOptions] = await Promise.all([
return res.status(404).end('not found') resolvePostData(post, ctx.appviewAgent),
} getModerationOptions(ctx.appviewAgent),
])
const data = await resolvePostData(post, ctx.appviewAgent) const svg = await satori(
const svg = await satori(<Post post={post} data={data} />, { <Post
post={post}
data={postData}
moderation={moderatePost(post, moderationOptions)}
/>,
{
fonts: ctx.fonts, fonts: ctx.fonts,
width: WIDTH, width: WIDTH,
loadAdditionalAsset: async (code, text) => { loadAdditionalAsset: async (code, text) => {
@@ -60,7 +64,8 @@ export default function (ctx: AppContext, app: Express) {
return await loadEmojiAsSvg(text) return await loadEmojiAsSvg(text)
} }
}, },
}) },
)
const output = await resvg.renderAsync(svg, { const output = await resvg.renderAsync(svg, {
fitTo: { fitTo: {
mode: 'width', mode: 'width',
@@ -68,10 +73,16 @@ export default function (ctx: AppContext, app: Express) {
}, },
logLevel: 'trace', logLevel: 'trace',
}) })
res.statusCode = 200 res.statusCode = 200
res.setHeader('content-type', 'image/png') res.setHeader('content-type', 'image/png')
res.setHeader('cdn-tag', [...data.images.keys()].join(',')) res.setHeader('cdn-tag', [...postData.images.keys()].join(','))
return res.end(output.asPng()) return res.end(output.asPng())
} catch (err) {
httpLogger.warn({err, uri: uri.toString()}, 'could not fetch post')
return res.status(404).end('not found')
}
}), }),
) )
} }
+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)
}