Moderation and cleanup
This commit is contained in:
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user