Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: labels #64

Merged
merged 15 commits into from
Mar 20, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions components/fires-server/adonisrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export default defineConfig({
() => import('#start/routes'),
() => import('#start/kernel'),
() => import('#start/events'),
() => import('#start/view'),
],

/*
Expand Down
44 changes: 44 additions & 0 deletions components/fires-server/app/controllers/api/labels_controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { HttpContext } from '@adonisjs/core/http'

import Label from '#models/label'
import { createLabelValidator, updateLabelValidator } from '#validators/label'

export default class LabelsController {
/**
* Handle form submission for the create action
*/
async store({ request, response, i18n }: HttpContext) {
const data = await request.validateUsing(createLabelValidator)
const language = i18n.locale

const label = await Label.create({
...data,
language,
})

return response.json(label.serialize())
}

/**
* Handle form submission for the edit action
*/
async update({ request, response, i18n }: HttpContext) {
const { params, ...update } = await request.validateUsing(updateLabelValidator)
const label = await Label.findOrFail(params.id)

await label.merge({ ...update, language: i18n.locale }).save()

return response.json(label.serialize())
}

/**
* Delete record
*/
async destroy({ params, response }: HttpContext) {
const label = await Label.findOrFail(params.id)

await label.delete()

return response.noContent()
}
}
36 changes: 36 additions & 0 deletions components/fires-server/app/controllers/labels_controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { HttpContext } from '@adonisjs/core/http'
import Label from '#models/label'
import { LabelsSerializer } from '#serializers/labels_serializer'

export default class LabelsController {
async index({ response, view }: HttpContext) {
const labels = await Label.all()
return response.negotiate(
{
json() {
response.json(LabelsSerializer.collection(labels))
},
html() {
return view.render('labels/index', { labels })
},
},
{ defaultHandler: 'html' }
)
}

async show({ params, response, view }: HttpContext) {
const label = await Label.findOrFail(params.id)

return response.negotiate(
{
json() {
response.json(LabelsSerializer.singular(label))
},
html() {
return view.render('labels/show', { label })
},
},
{ defaultHandler: 'html' }
)
}
}
31 changes: 31 additions & 0 deletions components/fires-server/app/models/label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { DateTime } from 'luxon'
import { BaseModel, beforeCreate, column } from '@adonisjs/lucid/orm'
import { v7 as uuidv7 } from 'uuid'

export default class Label extends BaseModel {
@column({ isPrimary: true })
declare id: string

@column()
declare language: string

@column()
declare name: string

@column()
declare summary: string

@column()
declare description: string

@column.dateTime({ autoCreate: true })
declare createdAt: DateTime

@column.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime

@beforeCreate()
static assignId(label: Label) {
label.id = uuidv7()
}
}
53 changes: 53 additions & 0 deletions components/fires-server/app/serializers/labels_serializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import Label from '#models/label'
import env from '#start/env'
import router from '@adonisjs/core/services/router'
import markdown from '#utils/markdown'

interface LabelType {
'@context'?: (string | Record<string, string>)[]
'id': string
'type': 'Label'
'name': string
'content'?: string
'summary'?: string
}

export class LabelsSerializer {
static collection(labels: Label[]) {
return {
'@context': [
'https://www.w3.org/ns/activitystreams',
{
Label: 'https://fires.fedimod.org/ns#Label',
},
],
'summary': `Labels from ${env.get('PUBLIC_URL')}`,
'type': 'Collection',
'id': new URL(router.makeUrl('labels.index'), env.get('PUBLIC_URL')).href,
'totalItems': labels.length,
'items': labels.map((item) => this.singular(item, false)),
}
}

static singular(item: Label, includeContext: boolean = true) {
const result: Partial<LabelType> = {}

if (includeContext) {
result['@context'] = [
'https://www.w3.org/ns/activitystreams',
{
Label: 'https://fires.fedimod.org/ns#Label',
},
]
}

result.id = new URL(router.makeUrl('labels.show', { id: item.id }), env.get('PUBLIC_URL')).href
result.type = 'Label'
result.name = item.name

if (item.summary) result.summary = item.summary
if (item.description) result.content = markdown.render(item.description)

return result
}
}
18 changes: 18 additions & 0 deletions components/fires-server/app/utils/markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import markdownit from 'markdown-it'

// enable everything
const md = markdownit({
html: false,
linkify: true,
typographer: true,
})

md.linkify.set({ fuzzyEmail: false })

function render(markdown: string): string {
return md.render(markdown)
}

export default {
render,
}
24 changes: 24 additions & 0 deletions components/fires-server/app/validators/label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Label from '#models/label'
import vine from '@vinejs/vine'

export const labelSchema = vine.object({
name: vine.string().unique({
table: Label.table,
column: 'name',
caseInsensitive: true,
}),
summary: vine.string().optional(),
description: vine.string().optional(),
})

export const createLabelValidator = vine.compile(labelSchema.clone())

export const updateLabelValidator = vine.compile(
vine.object({
params: vine.object({
id: vine.string().uuid({ version: [4] }),
}),

...labelSchema.getProperties(),
})
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { BaseSchema } from '@adonisjs/lucid/schema'

export default class extends BaseSchema {
protected tableName = 'labels'

async up() {
this.schema.createTable(this.tableName, (table) => {
table.uuid('id').primary()

// Based on the maximum length of a BCP-47 language tag:
// https://www.rfc-editor.org/rfc/rfc5646#section-4.4.1
table.string('language', 35).notNullable().comment('BCP-47 language tag')

table.text('name').notNullable()
table.text('summary')
table.text('description')

table.timestamp('created_at')
table.timestamp('updated_at')
})
}

async down() {
this.schema.dropTable(this.tableName)
}
}
76 changes: 76 additions & 0 deletions components/fires-server/database/seeders/example_data_seeder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import Label from '#models/label'
import { BaseSeeder } from '@adonisjs/lucid/seeders'
import dedent from 'dedent'

export default class extends BaseSeeder {
static environment = ['development']

async run() {
await Label.updateOrCreateMany('name', [
{
name: 'CSAM',
summary: 'Child Sexual Abuse Material',
description: dedent`
Imagery or videos which show a person who is a child and engaged in or is depicted as being engaged in explicit sexual activity.

Note: It is inappropriate to refer to this material as pornography. Although some laws refer to “child pornography”, pornography implies consent, which a child can never give.

Also known as: CP, CSEI, CSAI, SG-CSAM, CG-CSAM

## Library Resources:
- [CSAM Primer](https://connect.iftas.org/library/legal-regulatory/csam-primer/)
- [CSAM Reporting Requirements](https://connect.iftas.org/library/legal-regulatory/csam-reporting-requirements/)
- [Tech Coalition's Industry Classification System](https://paragonn-cdn.nyc3.cdn.digitaloceanspaces.com/technologycoalition.org/uploads/Tech_Coalition_Industry_Classification_System.pdf)
- [ECPAT's Terminology Guidelines](https://ecpat.org/wp-content/uploads/2021/05/Terminology-guidelines-396922-EN-1.pdf)

Source: the [IFTAS Trust & Safety Library](https://connect.iftas.org/library/content/csam/) - supporting volunteer moderators in the Fediverse
`,
language: 'en',
},
{
name: 'NCII',
summary:
'Non-Consensual Intimate Imagery: Non-consensual image sharing, or non-consensual intimate image sharing (also called “non-consensual explicit imagery” or colloquially called “revenge porn”), refers to the act or threat of creating, publishing or sharing an intimate image or video without the consent of the individuals visible in it.',
description: dedent`
Non-consensual intimate imagery, often referred to as “revenge porn,” involves sharing or distributing sexually explicit materials without the consent of the individual featured. This practice infringes on privacy and can lead to significant emotional and social harm for the victims. As technology has become more integral to forming and maintaining relationships, sharing intimate images has become more common. Unfortunately, this can lead to privacy violations if such content is shared without consent, whether it was initially shared willingly or captured without permission. Misuse of intimate images can involve blackmail, manipulation, or threats, causing distress, humiliation, potential outing of sexual orientation, job loss, and financial hardship.

## Challenges for Content Moderators

Quickly identifying and removing NCII to minimise harm, while ensuring that reports are accurate.
Providing clear channels for victims to report violations and ensuring they receive appropriate support.
Adhering to various laws and regulations that govern the sharing of such content.

## Tools and Resources

- [CCRI Safety Center](https://cybercivilrights.org/ccri-safety-center/): If you are a victim or survivor of image-based sexual abuse, you may want some help deciding what to do next.
- [StopNCII.org](https://stopncii.org): StopNCII.org is a free tool designed to support victims of Non-Consensual Intimate Image (NCII) abuse.

## Example Rule

Sharing intimate images without explicit consent from the individuals involved is strictly prohibited. We are committed to protecting our community’s privacy and safety, and any violation of this policy will result in immediate account suspension and potential legal action.

Source: the [IFTAS Trust & Safety Library](https://connect.iftas.org/library/content/ncii/) - supporting volunteer moderators in the Fediverse
`,
language: 'en',
},
{
name: 'Spam',
summary:
'Unsolicited, low-quality communications, often (but not necessarily) high-volume commercial solicitations, sent through a range of electronic media, including email, messaging, and social media.',
description: dedent`
## Resources:

- [Naive Bayes](https://en.wikipedia.org/wiki/Naive_Bayes_classifier)
- [Spam User Name Text Strings](https://connect.iftas.org/library/iftas-documentation/spam-user-name-text-strings/)

## Example Rule

Posting or distributing unsolicited or repetitive messages, commonly known as spam, is prohibited. This includes advertising content, phishing attempts, and irrelevant postings.

Source: the [IFTAS Trust & Safety Library](https://connect.iftas.org/library/content/spam/) - supporting volunteer moderators in the Fediverse
`,
language: 'en',
},
])
}
}
14 changes: 11 additions & 3 deletions components/fires-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@
"#models/*": "./app/models/*.js",
"#mails/*": "./app/mails/*.js",
"#services/*": "./app/services/*.js",
"#serializers/*": "./app/serializers/*.js",
"#listeners/*": "./app/listeners/*.js",
"#events/*": "./app/events/*.js",
"#middleware/*": "./app/middleware/*.js",
"#validators/*": "./app/validators/*.js",
"#utils/*": "./app/utils/*.js",
"#providers/*": "./providers/*.js",
"#policies/*": "./app/policies/*.js",
"#abilities/*": "./app/abilities/*.js",
Expand All @@ -44,6 +46,7 @@
"@japa/runner": "^4.2.0",
"@swc/core": "1.10.18",
"@types/luxon": "^3.4.2",
"@types/markdown-it": "^14.1.2",
"@types/node": "^22.13.2",
"eslint": "^9.20.1",
"hot-hook": "^0.4.0",
Expand All @@ -53,7 +56,8 @@
"rimraf": "^6.0.1",
"ts-node-maintained": "^10.9.5",
"typescript": "~5.7",
"vite": "^6.1.1"
"vite": "^6.1.1",
"yalc": "1.0.0-pre.53"
},
"dependencies": {
"@adonisjs/auth": "^9.3.1",
Expand All @@ -64,13 +68,17 @@
"@adonisjs/shield": "^8.1.2",
"@adonisjs/static": "^1.1.1",
"@adonisjs/vite": "^4.0.0",
"@fedify/fedify": "^1.4.6",
"@picocss/pico": "^2.0.6",
"@thisismissem/adonisjs-respond-with": "^2.0.0",
"@thisismissem/adonisjs-respond-with": "^2.1.0",
"@vinejs/vine": "^3.0.0",
"dedent": "^1.5.3",
"edge.js": "^6.2.1",
"luxon": "^3.5.0",
"markdown-it": "^14.1.0",
"pg": "^8.13.3",
"reflect-metadata": "^0.2.2"
"reflect-metadata": "^0.2.2",
"uuid": "^11.1.0"
},
"hotHook": {
"boundaries": [
Expand Down
Loading
Loading