From 637f10f2a294559c4108de507edd2ce40edfa5af Mon Sep 17 00:00:00 2001 From: Leah Date: Wed, 5 Feb 2025 17:21:32 +0100 Subject: [PATCH 1/2] Did all the groundwork I think for post languages --- .../1706827327619-add-language-selector.js | 11 + .../backend/src/core/NoteCreateService.ts | 11 + .../core/activitypub/models/ApNoteService.ts | 51 +- packages/backend/src/core/activitypub/type.ts | 3 + .../src/core/entities/NoteEntityService.ts | 1 + packages/backend/src/models/Note.ts | 6 + .../backend/src/models/json-schema/note.ts | 6 + .../processors/ExportNotesProcessorService.ts | 1 + .../src/server/api/endpoints/notes/create.ts | 7 + packages/backend/src/types.ts | 1 + packages/frontend/src/components/MkMenu.vue | 61 +- .../frontend/src/components/MkPopupMenu.vue | 3 +- .../frontend/src/components/MkPostForm.vue | 112 ++++ .../src/components/MkPostFormDialog.vue | 1 + packages/frontend/src/os.ts | 2 + packages/frontend/src/scripts/langmap.ts | 628 ++++++++++++++++++ packages/frontend/src/store.ts | 5 + packages/misskey-js/etc/misskey-js.api.md | 3 + packages/misskey-js/src/autogen/types.ts | 4 + packages/misskey-js/src/consts.ts | 94 +++ packages/misskey-js/src/index.ts | 1 + 21 files changed, 989 insertions(+), 23 deletions(-) create mode 100644 packages/backend/migration/1706827327619-add-language-selector.js diff --git a/packages/backend/migration/1706827327619-add-language-selector.js b/packages/backend/migration/1706827327619-add-language-selector.js new file mode 100644 index 000000000..840ec7711 --- /dev/null +++ b/packages/backend/migration/1706827327619-add-language-selector.js @@ -0,0 +1,11 @@ +export class AddLanguageSelector1706827327619 { + async up(queryRunner) { + await queryRunner.query( + `ALTER TABLE "note" ADD "lang" character varying(10)`, + ); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "lang"`); + } +} diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 8a43e100d..1d588342e 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -72,6 +72,7 @@ import { trackPromise } from '@/misc/promise-tracker.js'; import { isNotNull } from '@/misc/is-not-null.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { CollapsedQueue } from '@/misc/collapsed-queue.js'; +import {langmap} from "@/misc/langmap.js"; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -342,6 +343,15 @@ export class NoteCreateService implements OnApplicationShutdown { data.text = null; } + if (data.lang) { + if (!Object.keys(langmap).includes(data.lang.toLowerCase())) { + throw new Error("invalid lang param"); + } + data.lang = data.lang.toLowerCase(); + } else { + data.lang = null; + } + let tags = data.apHashtags; let emojis = data.apEmojis; let mentionedUsers = data.apMentions; @@ -458,6 +468,7 @@ export class NoteCreateService implements OnApplicationShutdown { : null, name: data.name, text: data.text, + lang: data.lang, hasPoll: data.poll != null, cw: data.cw ?? null, tags: tags.map(tag => normalizeForSearch(tag)), diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 9effcfac9..7eee22e4e 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -39,6 +39,7 @@ import { ApQuestionService } from './ApQuestionService.js'; import { ApImageService } from './ApImageService.js'; import type { Resolver } from '../ApResolverService.js'; import type { IObject, IPost } from '../type.js'; +import {Obj} from "@/misc/json-schema.js"; @Injectable() export class ApNoteService { @@ -204,12 +205,17 @@ export class ApNoteService { // テキストのパース let text: string | null = null; - if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { - text = note.source.content; + let lang: string | null = null; + if (note.source?.mediaType === 'text/x.misskeymarkdown' && (typeof note.source.content === 'string' || note.source.contentMap)) { + const guessed = this.guessLang(note.source); + text = guessed.text; + lang = guessed.lang; } else if (typeof note._misskey_content !== 'undefined') { text = note._misskey_content; - } else if (typeof note.content === 'string') { - text = this.apMfmService.htmlToMfm(note.content, note.tag); + } else if (typeof note.content === 'string' || note.contentMap) { + const guessed = this.guessLang(note); + lang = guessed.lang; + if (guessed.text) text = this.apMfmService.htmlToMfm(guessed.text, note.tag); } const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined); @@ -343,6 +349,7 @@ export class ApNoteService { name: note.name, cw, text, + lang, localOnly: false, visibility, visibleUsers, @@ -460,4 +467,40 @@ export class ApNoteService { }).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0])); })); } + + @bindThis + private guessLang(source: { contentMap?: Record, content?: string | null }): { lang: string | null, text: string | null } { + // do we have a map? + if (source.contentMap) { + const entries = Object.entries(source.contentMap); + + // only one entry: take that + if (entries.length === 1) { + return { lang: entries[0][0], text: entries[0][1] }; + } + + // did the sender indicate a preferred language? + if (source.content) { + for (const e of entries) { + if (e[1] === source.content) { + return { lang: e[0], text: e[1] }; + } + } + } + + // can we find one of *our* preferred languages? + // for (const prefLang of this.config.langPref) { + // if (source.contentMap[prefLang]) { + // return { lang: prefLang, text: source.contentMap[prefLang] }; + // } + // } + + // bah, just pick one + return { lang: entries[0][0], text: entries[0][1] }; + } + + // no map, so we don't know the language, just take whatever + // content we got + return { lang: null, text: source.content ?? null }; + } } diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 16812b7a4..aa1d7c306 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -11,6 +11,7 @@ export interface IObject { type: string | string[]; id?: string; name?: string | null; + contentMap?: Record; summary?: string; _misskey_summary?: string; published?: string; @@ -122,6 +123,7 @@ export interface IPost extends IObject { type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video' | 'Event'; source?: { content: string; + contentMap?: Record; mediaType: string; }; _misskey_quote?: string; @@ -134,6 +136,7 @@ export interface IQuestion extends IObject { actor: string; source?: { content: string; + contentMap?: Record; mediaType: string; }; _misskey_quote?: string; diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index f111b6540..b1526d468 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -331,6 +331,7 @@ export class NoteEntityService implements OnModuleInit { userId: note.userId, user: this.userEntityService.pack(note.user ?? note.userId, me), text: text, + lang: note.lang, cw: note.cw, visibility: note.visibility, localOnly: note.localOnly, diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts index 7803a68c7..0b2d3169d 100644 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -66,6 +66,12 @@ export class MiNote { }) public text: string | null; + @Column('varchar', { + length: 10, + nullable: true, + }) + public lang: string | null; + @Column('varchar', { length: 256, nullable: true, }) diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index 2641161c8..8f2fee0f0 100644 --- a/packages/backend/src/models/json-schema/note.ts +++ b/packages/backend/src/models/json-schema/note.ts @@ -2,6 +2,7 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ +import { langmap } from "@/misc/langmap.js"; export const packedNoteSchema = { type: 'object', @@ -26,6 +27,11 @@ export const packedNoteSchema = { type: 'string', optional: false, nullable: true, }, + lang: { + type: 'string', + enum: [...Object.keys(langmap)], + nullable: true, + }, cw: { type: 'string', optional: true, nullable: true, diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts index b45d4165c..4c530ac32 100644 --- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts @@ -45,6 +45,7 @@ class NoteStream extends ReadableStream> { return { id: note.id, text: note.text, + lang: note.lang, createdAt: idService.parse(note.id).date.toISOString(), fileIds: note.fileIds, files: files, diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index ddccbdae5..09b9ff5aa 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -23,6 +23,7 @@ import { isQuote, isRenote } from '@/misc/is-renote.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { LoggerService } from '@/core/LoggerService.js'; import { ApiError } from '../../error.js'; +import {langmap} from "@/misc/langmap.js"; export const meta = { tags: ['notes'], @@ -210,6 +211,11 @@ export const paramDef = { maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true, }, + lang: { + type: "string", + enum: Object.keys(langmap), + nullable: true, + }, fileIds: { type: 'array', uniqueItems: true, @@ -484,6 +490,7 @@ export default class extends Endpoint { // eslint- expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null, } : undefined, text: ps.text ?? undefined, + lang: ps.lang, reply, renote, cw: ps.cw, diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index dc13af42f..00c8dcace 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -375,6 +375,7 @@ export type NoteCreateOption = { scheduledAt?: Date | null; name?: string | null; text?: string | null; + lang?: string | null; reply?: MiNote | null; renote?: MiNote | null; files?: MiDriveFile[] | null; diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue index d91239b9e..8decb8f2d 100644 --- a/packages/frontend/src/components/MkMenu.vue +++ b/packages/frontend/src/components/MkMenu.vue @@ -12,8 +12,15 @@ SPDX-License-Identifier: AGPL-3.0-only :style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }" @contextmenu.self="e => e.preventDefault()" > +
+ + + +