Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions src/modules/lexical/lexical.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { bioConfig } from './ppsl-cd-lexical-shared/src/editors/Bio/config.js'
import { defaultTheme, readOnlyTheme } from './ppsl-cd-lexical-shared/src/editors/theme.js'
import { entityConfig } from './ppsl-cd-lexical-shared/src/editors/Entity/config.js'
import { $isEntityContainerNode } from './ppsl-cd-lexical-shared/src/editors/plugins/EntityContainer/node.js'
import { $isEntityImageNode } from './ppsl-cd-lexical-shared/src/editors/plugins/EntityImage/node.js'
import { $isEntityImageNode, EntityImageNode } from './ppsl-cd-lexical-shared/src/editors/plugins/EntityImage/node.js'
import { $isEntityShortDescriptionNode } from './ppsl-cd-lexical-shared/src/editors/plugins/EntityShortDescription/node.js'
import { $isEntityLongDescriptionNode } from './ppsl-cd-lexical-shared/src/editors/plugins/EntityLongDescription/node.js'
import { EntityMentionNode } from './ppsl-cd-lexical-shared/src/editors/plugins/EntityMention/node.js'
import { getIfImageIsNSFW } from '../wikimedia-commons/nsfwFilter.controller.js'

const {
$getRoot,
Expand Down Expand Up @@ -98,6 +99,9 @@ function validateEntityEditor (stringifiedJSON) {
sanitizeNode(entityEditor, root)

// Make sure last child is entity-container
/**
* @type {lexical.ElementNode}
*/
const entityContainer = root.getLastChild()
if (!$isEntityContainerNode(entityContainer)) {
throw new Error(`First child is "${entityContainer.getType()}" and not "entity-container".`)
Expand All @@ -107,8 +111,11 @@ function validateEntityEditor (stringifiedJSON) {
if (root.getChildrenSize() > 1) {
throw new Error('Root has too many children.')
}

// Make sure first child of entity-container is entity-image
/**
* @type {EntityImageNode}
*/
const entityImage = entityContainer.getFirstChild()
if (!$isEntityImageNode(entityImage)) {
throw new Error(`First child of "entity-container" is "${entityImage.getType()}" and not "entity-image".`)
Expand All @@ -128,6 +135,14 @@ function validateEntityEditor (stringifiedJSON) {
throw new Error(`Last child of "entity-container" is "${entityImage.getType()}" and not "entity-long-description".`)
}

// Make sure the inserted image is not NSFW
const entityNSFWImageURL = new URL(entityImage.getSrc())
const fileName = `File:${entityNSFWImageURL.pathname.split('/').pop().replace(/_/g, ' ')}`
const request = {query:{image:fileName}}
if (getIfImageIsNSFW(request, null)) {
throw new Error(`Entity Image is NSFW "${entityImage.getSrc()}".`)
}

onlyTextNodes(entityEditor, entityLongDescription.getChildren())

resolve(entityEditor)
Expand Down
35 changes: 35 additions & 0 deletions src/modules/wikimedia-commons/nsfwFilter.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { wikibaseGetEntities, wikidataSparqlQueryDepicts } from './nsfwFilter.service.js'

/**
* https://commons.m.wikimedia.org/wiki/MediaWiki:Gadget-NSFW.js
* @param {Fastify.Request} request
* @param {Fastify.Reply} reply
*/
export async function getIfImageIsNSFW (request, reply) {
const { image } = request.query
const data = await wikibaseGetEntities(image)
if (data.entities === undefined) {
return
}
const imagesData = []
for (const pageMid in data.entities) {
const entity = data.entities[pageMid]
if (entity.statements === undefined || entity.statements.P180 === undefined) {
continue
}
imagesData.push({
depicts: entity.statements.P180.map(function (value) {
return value.mainsnak.datavalue.value.id
})
})
}
let topics = imagesData.map(function (imageData) {
return imageData.depicts
}).flat()
topics = topics.filter(function (value, index, self) {
return self.indexOf(value) === index
})
const sparqlQueryResults = await wikidataSparqlQueryDepicts(topics)

return !!sparqlQueryResults.results.bindings.length
}
18 changes: 18 additions & 0 deletions src/modules/wikimedia-commons/nsfwFilter.route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { getIfImageIsNSFW } from './nsfwFilter.controller.js'
import { $ref } from './nsfwFilter.schema.js'

/**
* @param {Fastify.Instance} fastify
*/
export default async function nsfwFilterRoutes (fastify) {
fastify.get('/nsfwFilter', {
schema: {
querystring: $ref('nsfwInput')
/**
response: {
200: $ref('ResponseSchema')
}
*/
}
}, getIfImageIsNSFW)
}
16 changes: 16 additions & 0 deletions src/modules/wikimedia-commons/nsfwFilter.schema.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { z } from 'zod'
import { buildJsonSchemas } from 'fastify-zod'

export const nsfwInput = z.object({
image: z.string()
})

// Build

export const { schemas: nsfwFilterSchemas, $ref } = buildJsonSchemas({
nsfwInput
}, { $id: 'nsfwFilter' })

/**
* @typedef {z.infer<typeof nsfwInput>} NSFWInputSchema
*/
54 changes: 54 additions & 0 deletions src/modules/wikimedia-commons/nsfwFilter.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
const headers = new Headers({ 'user-agent': 'ppsl-cd' })
const WIKIMEDIA_API_ENDPOINT = 'https://commons.wikimedia.org/w/api.php'
const nsfwTopics = [
'Q291', // pornography
'Q496', // feces
'Q608', // human sexual activity
'Q5880', // vagina
'Q5887', // orgasm
'Q9103', // breast
'Q10791', // nudity
'Q10816', // sex toy
'Q40446', // nude
'Q42165', // buttocks
'Q124490', // violence
'Q133993', // erection
'Q174471', // scrotum
'Q181001', // erotica
'Q188641', // nipple
'Q650891', // glans penis
'Q673203', // foreskin
'Q843533', // areola
'Q844482', // killing
'Q1058795', // body fluid
'Q1406501', // labia
'Q2148678', // sexual penetration
'Q2192288', // vulva
'Q3258546', // human anus
'Q4620674', // sex organ
'Q11722446' // mons pubis
]

export async function wikibaseGetEntities (imageTitle) {
const url = new URL(WIKIMEDIA_API_ENDPOINT)
url.searchParams.append('action', 'wbgetentities')
url.searchParams.append('props', 'info|claims')
url.searchParams.append('sites', 'commonswiki')
url.searchParams.append('titles', imageTitle)
url.searchParams.append('format', 'json')
const request = await fetch(url)
return await request.json()
}

export async function wikidataSparqlQueryDepicts (topics) {
const sparqlQuery = `SELECT ?depicts WHERE {
?depicts wdt:P31*/wdt:P279* ?nsfw .
VALUES ?depicts {wd:${topics.join(' wd:')}}
VALUES ?nsfw {wd:${nsfwTopics.join(' wd:')}}
}`
const url = new URL('https://query.wikidata.org/sparql')
url.searchParams.append('format', 'json')
url.searchParams.append('query', sparqlQuery)
const request = await fetch(url, { headers })
return await request.json()
}
2 changes: 2 additions & 0 deletions src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import fp from 'fastify-plugin'
import userRoutes from './modules/user/user.route.js'
import postRoutes from './modules/post/post.route.js'
import lexicalRoutes from './modules/lexical/lexical.route.js'
import nsfwFilterRoutes from './modules/wikimedia-commons/nsfwFilter.route.js'

/**
* @type {import('fastify').FastifyPluginAsync}
Expand All @@ -11,6 +12,7 @@ const routes = fp(async (fastify) => {
fastify.register(userRoutes, { prefix: 'api/users' })
fastify.register(postRoutes, { prefix: 'api/posts' })
fastify.register(lexicalRoutes, { prefix: 'api/lexical' })
fastify.register(nsfwFilterRoutes, { prefix: 'api' })
})

export default routes
3 changes: 2 additions & 1 deletion src/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import fp from 'fastify-plugin'

import { userSchemas } from './modules/user/user.schema.js'
import { postSchemas } from './modules/post/post.schema.js'
import { nsfwFilterSchemas } from './modules/wikimedia-commons/nsfwFilter.schema.js'

/**
* @type {import('fastify').FastifyPluginAsync}
*/
const schemas = fp(async (fastify) => {
for (const schema of [...userSchemas, ...postSchemas]) {
for (const schema of [...userSchemas, ...postSchemas, ...nsfwFilterSchemas]) {
await fastify.addSchema(schema)
}
})
Expand Down