Skip to content

Commit

Permalink
✨ Apply embed specific tag on subjects for video, image and external (#…
Browse files Browse the repository at this point in the history
…2703)

* ✨ Refactor subject tagging to facilitate video content tagging

* ♻️ Refactor tag check

* ✅ Fix tagging logic

* ♻️ Refactor content tagger and fix image content type check

* ✨ Add embed tag check for video and external

* ✨ Add tagging for both media and image embed
  • Loading branch information
foysalit authored Aug 29, 2024
1 parent 6bc7faf commit 372ed4c
Show file tree
Hide file tree
Showing 9 changed files with 336 additions and 166 deletions.
13 changes: 7 additions & 6 deletions packages/ozone/src/api/moderation/emitEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from '../../lexicon/types/tools/ozone/moderation/defs'
import { HandlerInput } from '../../lexicon/types/tools/ozone/moderation/emitEvent'
import { subjectFromInput } from '../../mod-service/subject'
import { ModerationLangService } from '../../mod-service/lang'
import { TagService } from '../../tag-service'
import { retryHttp } from '../../util'
import { ModeratorOutput, AdminTokenOutput } from '../../auth-verifier'

Expand Down Expand Up @@ -137,12 +137,13 @@ const handleModerationEvent = async ({
createdBy,
})

const moderationLangService = new ModerationLangService(moderationTxn)
await moderationLangService.tagSubjectWithLang({
const tagService = new TagService(
subject,
createdBy: ctx.cfg.service.did,
subjectStatus: result.subjectStatus,
})
result.subjectStatus,
ctx.cfg.service.did,
moderationTxn,
)
await tagService.evaluateForSubject()

if (subject.isRepo()) {
if (isTakedownEvent) {
Expand Down
11 changes: 6 additions & 5 deletions packages/ozone/src/api/report/createReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getReasonType } from '../util'
import { subjectFromInput } from '../../mod-service/subject'
import { REASONAPPEAL } from '../../lexicon/types/com/atproto/moderation/defs'
import { ForbiddenError } from '@atproto/xrpc-server'
import { ModerationLangService } from '../../mod-service/lang'
import { TagService } from '../../tag-service'

export default function (server: Server, ctx: AppContext) {
server.com.atproto.moderation.createReport({
Expand All @@ -31,12 +31,13 @@ export default function (server: Server, ctx: AppContext) {
reportedBy: requester || ctx.cfg.service.did,
})

const moderationLangService = new ModerationLangService(moderationTxn)
await moderationLangService.tagSubjectWithLang({
const tagService = new TagService(
subject,
subjectStatus,
createdBy: ctx.cfg.service.did,
})
ctx.cfg.service.did,
moderationTxn,
)
await tagService.evaluateForSubject()

return reportEvent
})
Expand Down
30 changes: 30 additions & 0 deletions packages/ozone/src/tag-service/content-tagger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ModerationService } from '../mod-service'
import { ModSubject } from '../mod-service/subject'
import { ModerationSubjectStatusRow } from '../mod-service/types'

export abstract class ContentTagger {
constructor(
protected subject: ModSubject,
protected subjectStatus: ModerationSubjectStatusRow | null,
protected moderationService: ModerationService,
) {}

protected abstract tagPrefix: string

protected abstract isApplicable(): boolean
protected abstract buildTags(): Promise<string[]>

async getTags(): Promise<string[]> {
if (!this.isApplicable()) {
return []
}

return this.buildTags()
}

protected tagAlreadyExists(): boolean {
return Boolean(
this.subjectStatus?.tags?.some((tag) => tag.startsWith(this.tagPrefix)),
)
}
}
68 changes: 68 additions & 0 deletions packages/ozone/src/tag-service/embed-tagger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
AppBskyEmbedImages,
AppBskyEmbedRecordWithMedia,
AppBskyFeedPost,
AppBskyEmbedVideo,
AppBskyEmbedExternal,
} from '@atproto/api'
import { langLogger as log } from '../logger'
import { ContentTagger } from './content-tagger'
import { ids } from '../lexicon/lexicons'

export class EmbedTagger extends ContentTagger {
tagPrefix = 'embed:'

isApplicable(): boolean {
return (
!!this.subjectStatus &&
!this.tagAlreadyExists() &&
this.subject.isRecord() &&
this.subject.parsedUri.collection === ids.AppBskyFeedPost
)
}

async buildTags(): Promise<string[]> {
try {
const recordValue = await this.getRecordValue()
if (!recordValue) {
return []
}
const tags: string[] = []
if (AppBskyFeedPost.isRecord(recordValue)) {
const embedContent = AppBskyEmbedRecordWithMedia.isMain(
recordValue.embed,
)
? recordValue.embed.media
: recordValue.embed

if (AppBskyEmbedImages.isMain(embedContent)) {
tags.push(`${this.tagPrefix}image`)
}

if (AppBskyEmbedVideo.isMain(embedContent)) {
tags.push(`${this.tagPrefix}video`)
}

if (AppBskyEmbedExternal.isMain(embedContent)) {
tags.push(`${this.tagPrefix}external`)
}
}
return tags
} catch (err) {
log.error({ subject: this.subject, err }, 'Error getting record langs')
return []
}
}

async getRecordValue(): Promise<Record<string, unknown> | undefined> {
if (!this.subject.isRecord()) {
return undefined
}
const recordByUri = await this.moderationService.views.fetchRecords([
this.subject,
])

const record = recordByUri.get(this.subject.uri)
return record?.value
}
}
59 changes: 59 additions & 0 deletions packages/ozone/src/tag-service/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ModerationService } from '../mod-service'
import { ModSubject } from '../mod-service/subject'
import { langLogger as log } from '../logger'
import { ContentTagger } from './content-tagger'
import { LanguageTagger } from './language-tagger'
import { EmbedTagger } from './embed-tagger'
import { ModerationSubjectStatusRow } from '../mod-service/types'

export class TagService {
private taggers: ContentTagger[]

constructor(
private subject: ModSubject,
protected subjectStatus: ModerationSubjectStatusRow | null,
private taggerDid: string,
private moderationService: ModerationService,
) {
this.taggers = [
new LanguageTagger(subject, subjectStatus, moderationService),
new EmbedTagger(subject, subjectStatus, moderationService),
// Add more taggers as needed
]
}

async evaluateForSubject() {
try {
const tags: string[] = []

await Promise.all(
this.taggers.map(async (tagger) => {
try {
const newTags = await tagger.getTags()
if (newTags.length) tags.push(...newTags)
} catch (e) {
// Don't let one tagger error stop the rest from running
log.error(
{ subject: this.subject, err: e },
'Error applying tagger',
)
}
}),
)

if (tags.length > 0) {
await this.moderationService.logEvent({
event: {
$type: 'tools.ozone.moderation.defs#modEventTag',
add: tags,
remove: [],
},
subject: this.subject,
createdBy: this.taggerDid,
})
}
} catch (err) {
log.error({ subject: this.subject, err }, 'Error tagging subject')
}
}
}
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,26 @@ import {
AppBskyGraphList,
} from '@atproto/api'

import { ModerationService } from '.'
import { ModSubject } from './subject'
import { ModerationSubjectStatusRow } from './types'
import { langLogger as log } from '../logger'
import { code3ToCode2 } from './lang-data'
import { code3ToCode2 } from './language-data'
import { ContentTagger } from './content-tagger'

export class ModerationLangService {
constructor(private moderationService: ModerationService) {}
export class LanguageTagger extends ContentTagger {
tagPrefix = 'lang:'

async tagSubjectWithLang({
subject,
subjectStatus,
createdBy,
}: {
subject: ModSubject
createdBy: string
subjectStatus: ModerationSubjectStatusRow | null
}) {
if (
subjectStatus &&
!subjectStatus.tags?.find((tag) => tag.includes('lang:'))
) {
try {
const recordLangs = await this.getRecordLang({
subject,
})
await this.moderationService.logEvent({
event: {
$type: 'tools.ozone.moderation.defs#modEventTag',
add: recordLangs
? recordLangs.map((lang) => `lang:${lang}`)
: ['lang:und'],
remove: [],
},
subject,
createdBy,
})
} catch (err) {
log.error({ subject, err }, 'Error getting record langs')
}
isApplicable(): boolean {
return !!this.subjectStatus && !this.tagAlreadyExists()
}

async buildTags(): Promise<string[]> {
try {
const recordLangs = await this.getRecordLang()
return recordLangs
? recordLangs.map((lang) => `${this.tagPrefix}${lang}`)
: [`${this.tagPrefix}und`]
} catch (err) {
log.error({ subject: this.subject, err }, 'Error getting record langs')
return []
}
}

Expand All @@ -65,20 +45,16 @@ export class ModerationLangService {
return text?.trim()
}

async getRecordLang({
subject,
}: {
subject: ModSubject
}): Promise<string[] | null> {
const isRecord = subject.isRecord()
async getRecordLang(): Promise<string[] | null> {
const langs = new Set<string>()

if (
subject.isRepo() ||
(isRecord && subject.uri.endsWith('/app.bsky.actor.profile/self'))
this.subject.isRepo() ||
(this.subject.isRecord() &&
this.subject.uri.endsWith('/app.bsky.actor.profile/self'))
) {
const feed = await this.moderationService.views.fetchAuthorFeed(
subject.did,
this.subject.did,
)
feed.forEach((item) => {
const itemLangs = item.post.record['langs'] as string[] | null
Expand All @@ -89,11 +65,11 @@ export class ModerationLangService {
})
}

if (isRecord) {
if (this.subject.isRecord()) {
const recordByUri = await this.moderationService.views.fetchRecords([
subject,
this.subject,
])
const record = recordByUri.get(subject.uri)
const record = recordByUri.get(this.subject.uri)
const recordLang = record?.value.langs as string[] | null
const recordText = this.getTextFromRecord(record?.value)
if (recordLang?.length) {
Expand Down
Loading

0 comments on commit 372ed4c

Please sign in to comment.