Add moderation setup
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user