Moderation and cleanup

This commit is contained in:
Eric Bailey
2024-08-29 11:37:01 -05:00
parent d15585bea4
commit 998f4038b0
22 changed files with 523 additions and 218 deletions
+12
View File
@@ -0,0 +1,12 @@
import MIMEType from 'whatwg-mimetype'
export async function getImage(url: string) {
const response = await fetch(url)
const mimeType = new MIMEType(response.headers.get('content-type') ?? '')
const arrayBuf = await response.arrayBuffer() // must drain body even if it will be discarded
if (response.status !== 200) return null
return {
mime: mimeType.essence as string | undefined,
image: Buffer.from(arrayBuf),
}
}
+50
View File
@@ -0,0 +1,50 @@
/* eslint-disable no-restricted-imports */
import {
AppBskyLabelerDefs,
AtpAgent,
BSKY_LABELER_DID,
DEFAULT_LABEL_SETTINGS,
interpretLabelValueDefinitions,
moderatePost,
} from '@atproto/api'
export type ModeratorData = Awaited<ReturnType<typeof getModeratorData>>
export const DEFAULT_LABELS: typeof DEFAULT_LABEL_SETTINGS = Object.fromEntries(
Object.entries(DEFAULT_LABEL_SETTINGS).map(([key, _pref]) => [key, 'hide']),
)
export async function getModeratorData(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
const definitions = interpretLabelValueDefinitions(labeler)
const moderationOptions: Parameters<typeof moderatePost>[1] = {
userDid: undefined,
prefs: {
adultContentEnabled: false,
labels: DEFAULT_LABELS,
mutedWords: [],
hiddenPosts: [],
labelers: [
{
did: BSKY_LABELER_DID,
labels: DEFAULT_LABELS,
},
],
},
labelDefs: {
[BSKY_LABELER_DID]: definitions,
},
}
return {
labeler,
moderationOptions,
}
}
+14
View File
@@ -0,0 +1,14 @@
import {AtpAgent, AtUri} from '@atproto/api'
export async function getPost({uri, agent}: {uri: AtUri; agent: AtpAgent}) {
if (!uri.hostname.startsWith('did:')) {
const res = await agent.resolveHandle({
handle: uri.hostname,
})
uri.hostname = res.data.did
}
const {data} = await agent.getPosts({
uris: [uri.toString()],
})
return data.posts.at(0)
}
+322
View File
@@ -0,0 +1,322 @@
import {
AppBskyEmbedExternal,
AppBskyEmbedImages,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs,
AppBskyFeedPost,
AppBskyGraphDefs,
AtpAgent,
RichText,
} from '@atproto/api'
import {httpLogger} from '../logger.js'
import {getStarterPackImageUri} from '../util/getStarterPackImageUri.js'
import {getImage} from './getImage.js'
export type Metadata = {
aspectRatio: {
width: number
height: number
}
}
export type Image = Metadata & {
mime: 'image/jpeg' | 'image/png' | string | undefined
image: Buffer
}
export type PostData = {
images: Map<string, Image>
texts: Map<string, RichText>
}
function normalizeAspectRatio(aspectRatio?: {
width: number
height: number
}): Metadata['aspectRatio'] {
if (!aspectRatio)
return {
width: 1000,
height: 1000,
}
return aspectRatio
}
export async function getPostData(
post: AppBskyFeedDefs.PostView,
agent: AtpAgent,
): Promise<PostData> {
const images: Map<string, Metadata> = new Map()
const texts: Map<string, RichText> = new Map()
// console.log(JSON.stringify(post, null, 2))
if (post.author.avatar) {
images.set(post.author.avatar, {
aspectRatio: {
width: 1000,
height: 1000,
},
})
}
if (AppBskyFeedPost.isRecord(post.record) && post.record.text) {
texts.set(post.record.text, new RichText({text: post.record.text}))
}
if (post.embed) {
if (AppBskyEmbedImages.isView(post.embed)) {
// get OPs media
for (const image of post.embed.images) {
images.set(image.fullsize, {
aspectRatio: normalizeAspectRatio(image.aspectRatio),
})
}
}
if (AppBskyEmbedExternal.isView(post.embed)) {
if (post.embed.external.thumb) {
images.set(post.embed.external.thumb, {
aspectRatio: {
width: 1200,
height: 630,
},
})
}
}
if (AppBskyEmbedRecord.isView(post.embed)) {
if (AppBskyEmbedRecord.isViewRecord(post.embed.record)) {
if (post.embed.record.author.avatar) {
images.set(post.embed.record.author.avatar, {
aspectRatio: {
width: 1000,
height: 1000,
},
})
}
for (const embed of post.embed.record.embeds) {
if (AppBskyEmbedImages.isView(embed)) {
for (const image of embed.images) {
images.set(image.fullsize, {
aspectRatio: normalizeAspectRatio(image.aspectRatio),
})
}
}
if (AppBskyEmbedExternal.isView(embed)) {
if (embed.external.thumb) {
images.set(embed.external.thumb, {
aspectRatio: {
width: 1200,
height: 630,
},
})
}
}
if (AppBskyEmbedRecordWithMedia.isView(embed)) {
if (AppBskyEmbedImages.isView(embed.media)) {
for (const image of embed.media.images) {
images.set(image.fullsize, {
aspectRatio: normalizeAspectRatio(image.aspectRatio),
})
}
} else if (AppBskyEmbedExternal.isView(embed.media)) {
if (embed.media.external.thumb) {
images.set(embed.media.external.thumb, {
aspectRatio: {
width: 1200,
height: 630,
},
})
}
}
}
if (AppBskyGraphDefs.isListView(embed.record)) {
if (embed.record.avatar) {
images.set(embed.record.avatar, {
aspectRatio: {
width: 1000,
height: 1000,
},
})
}
}
if (AppBskyFeedDefs.isGeneratorView(embed.record)) {
if (embed.record.avatar) {
images.set(embed.record.avatar, {
aspectRatio: {
width: 1000,
height: 1000,
},
})
}
}
if (AppBskyGraphDefs.isStarterPackViewBasic(embed.record)) {
const uri = getStarterPackImageUri(embed.record)
images.set(uri, {
aspectRatio: {
width: 1200,
height: 630,
},
})
}
}
if (
AppBskyFeedPost.isRecord(post.embed.record.value) &&
post.embed.record.value.text
) {
texts.set(
post.embed.record.value.text,
new RichText({text: post.embed.record.value.text}),
)
}
}
if (AppBskyGraphDefs.isListView(post.embed.record)) {
if (post.embed.record.avatar) {
images.set(post.embed.record.avatar, {
aspectRatio: {
width: 1000,
height: 1000,
},
})
}
}
if (AppBskyFeedDefs.isGeneratorView(post.embed.record)) {
if (post.embed.record.avatar) {
images.set(post.embed.record.avatar, {
aspectRatio: {
width: 1000,
height: 1000,
},
})
}
}
if (AppBskyGraphDefs.isStarterPackViewBasic(post.embed.record)) {
const uri = getStarterPackImageUri(post.embed.record)
images.set(uri, {
aspectRatio: {
width: 1200,
height: 630,
},
})
}
}
if (AppBskyEmbedRecordWithMedia.isView(post.embed)) {
// get OPs media
if (AppBskyEmbedImages.isView(post.embed.media)) {
for (const image of post.embed.media.images) {
images.set(image.fullsize, {
aspectRatio: normalizeAspectRatio(image.aspectRatio),
})
}
}
if (AppBskyEmbedExternal.isView(post.embed.media)) {
if (post.embed.media.external.thumb) {
images.set(post.embed.media.external.thumb, {
aspectRatio: {
width: 1200,
height: 630,
},
})
}
}
// get media from embedded post
if (AppBskyEmbedRecord.isViewRecord(post.embed.record.record)) {
for (const embed of post.embed.record.record.embeds) {
if (AppBskyEmbedImages.isView(embed)) {
for (const image of embed.images) {
images.set(image.fullsize, {
aspectRatio: normalizeAspectRatio(image.aspectRatio),
})
}
}
if (AppBskyEmbedExternal.isView(embed)) {
if (embed.external.thumb) {
images.set(embed.external.thumb, {
aspectRatio: {
width: 1200,
height: 630,
},
})
}
}
if (AppBskyEmbedRecordWithMedia.isView(embed)) {
if (AppBskyEmbedImages.isView(embed.media)) {
for (const image of embed.media.images) {
images.set(image.fullsize, {
aspectRatio: normalizeAspectRatio(image.aspectRatio),
})
}
} else if (AppBskyEmbedExternal.isView(embed.media)) {
if (embed.media.external.thumb) {
images.set(embed.media.external.thumb, {
aspectRatio: {
width: 1200,
height: 630,
},
})
}
}
}
}
if (
AppBskyFeedPost.isRecord(post.embed.record.record.value) &&
post.embed.record.record.value.text
) {
texts.set(
post.embed.record.record.value.text,
new RichText({text: post.embed.record.record.value.text}),
)
}
}
}
}
const deduped = Array.from(images.entries())
const resolved = await Promise.all(
deduped.map(async ([url, meta]) => {
try {
const image = await getImage(url)
return [
url,
{
...meta,
...image,
},
] as const
} catch (err) {
httpLogger.warn({err, uri: url}, 'could not fetch image')
return [
url,
{
...meta,
image: null,
},
] as const
}
}),
)
await Promise.all(Array.from(texts.values()).map(r => r.detectFacets(agent)))
const extracted = resolved.filter(([, i]) => i.image !== null) as [
string,
Image,
][]
return {
images: new Map(extracted),
texts,
}
}