From a76d3cf861eb631d703092262498720b3a59bced Mon Sep 17 00:00:00 2001 From: Essem Date: Sat, 3 Feb 2024 11:45:45 -0600 Subject: [PATCH 001/867] feat: Add language metadata to notes --- locales/en-US.yml | 1 + .../migration/1706852162173-add-post-lang.js | 13 + .../backend/src/core/NoteCreateService.ts | 12 +- packages/backend/src/core/NoteEditService.ts | 14 + .../src/core/activitypub/ApRendererService.ts | 8 +- .../core/activitypub/models/ApNoteService.ts | 21 + packages/backend/src/core/activitypub/type.ts | 1 + .../src/core/entities/NoteEntityService.ts | 5 +- packages/backend/src/misc/langmap.ts | 620 +++++------------- packages/backend/src/models/Note.ts | 6 + packages/backend/src/models/NoteEdit.ts | 32 +- .../backend/src/models/json-schema/note.ts | 6 + .../src/server/api/endpoints/notes/create.ts | 3 + .../src/server/api/endpoints/notes/edit.ts | 12 +- .../frontend/src/components/MkPostForm.vue | 78 +++ .../src/components/MkPostFormDialog.vue | 3 +- packages/frontend/src/scripts/langmap.ts | 620 +++++------------- packages/frontend/src/store.ts | 4 + packages/misskey-js/src/consts.ts | 93 +++ packages/misskey-js/src/index.ts | 1 + 20 files changed, 630 insertions(+), 923 deletions(-) create mode 100644 packages/backend/migration/1706852162173-add-post-lang.js diff --git a/locales/en-US.yml b/locales/en-US.yml index 64033d6a2b..caaada93b5 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1265,6 +1265,7 @@ hemisphere: "Where are you located" withSensitive: "Include notes with sensitive files" userSaysSomethingSensitive: "Post by {name} contains sensitive content" enableHorizontalSwipe: "Swipe to switch tabs" +noLanguage: "No language" _bubbleGame: howToPlay: "How to play" _howToPlay: diff --git a/packages/backend/migration/1706852162173-add-post-lang.js b/packages/backend/migration/1706852162173-add-post-lang.js new file mode 100644 index 0000000000..90e1338f12 --- /dev/null +++ b/packages/backend/migration/1706852162173-add-post-lang.js @@ -0,0 +1,13 @@ +export class AddPostLang1706852162173 { + name = 'AddPostLang1706852162173' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" ADD "lang" character varying(10)`); + await queryRunner.query(`ALTER TABLE "note_edit" ADD "lang" character varying(10)`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "lang"`); + await queryRunner.query(`ALTER TABLE "note_edit" DROP COLUMN "lang"`); + } +} diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 35a3305335..9f9f6547d1 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -63,6 +63,7 @@ import { trackPromise } from '@/misc/promise-tracker.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { isNotNull } from '@/misc/is-not-null.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { langmap } from '@/misc/langmap.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -133,6 +134,7 @@ type Option = { createdAt?: Date | null; name?: string | null; text?: string | null; + lang?: string | null; reply?: MiNote | null; renote?: MiNote | null; files?: MiDriveFile[] | null; @@ -337,6 +339,13 @@ export class NoteCreateService implements OnApplicationShutdown { data.text = null; } + if (data.lang) { + if (!Object.keys(langmap).includes(data.lang.toLowerCase())) throw new Error('invalid param'); + data.lang = data.lang.toLowerCase(); + } else { + data.lang = null; + } + let tags = data.apHashtags; let emojis = data.apEmojis; let mentionedUsers = data.apMentions; @@ -579,6 +588,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)), @@ -1004,7 +1014,7 @@ export class NoteCreateService implements OnApplicationShutdown { removeOnComplete: true, }); } - + // Pack the note const noteObj = await this.noteEntityService.pack(note, null, { skipHide: true, withReactionAndUserPairCache: true }); diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index 2561bbec27..3d8f52947f 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -52,6 +52,7 @@ import { isReply } from '@/misc/is-reply.js'; import { trackPromise } from '@/misc/promise-tracker.js'; import { isUserRelated } from '@/misc/is-user-related.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { langmap } from '@/misc/langmap.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention' | 'edited'; @@ -122,6 +123,7 @@ type Option = { createdAt?: Date | null; name?: string | null; text?: string | null; + lang?: string | null; reply?: MiNote | null; renote?: MiNote | null; files?: MiDriveFile[] | null; @@ -358,6 +360,13 @@ export class NoteEditService implements OnApplicationShutdown { data.text = null; } + if (data.lang) { + if (!Object.keys(langmap).includes(data.lang.toLowerCase())) throw new Error('invalid param'); + data.lang = data.lang.toLowerCase(); + } else { + data.lang = null; + } + let tags = data.apHashtags; let emojis = data.apEmojis; let mentionedUsers = data.apMentions; @@ -420,6 +429,9 @@ export class NoteEditService implements OnApplicationShutdown { if (oldnote.hasPoll !== !!data.poll) { update.hasPoll = !!data.poll; } + if (data.lang !== oldnote.lang) { + update.lang = data.lang; + } const poll = await this.pollsRepository.findOneBy({ noteId: oldnote.id }); @@ -434,6 +446,7 @@ export class NoteEditService implements OnApplicationShutdown { oldText: oldnote.text || undefined, newText: update.text || undefined, cw: update.cw || undefined, + lang: update.lang || undefined, fileIds: undefined, oldDate: exists ? oldnote.updatedAt as Date : this.idService.parse(oldnote.id).date, updatedAt: new Date(), @@ -453,6 +466,7 @@ export class NoteEditService 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/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index a84feffac6..5af8fed2ab 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -285,7 +285,7 @@ export class ApRendererService { if (instance && instance.softwareName === 'pleroma') isMastodon = true; } } - + const object: ILike = { type: 'Like', id: `${this.config.url}/likes/${noteReaction.id}`, @@ -454,6 +454,9 @@ export class ApRendererService { mediaType: 'text/x.misskeymarkdown', }, }), + contentMap: note.lang ? { + [note.lang]: content, + } : null, _misskey_quote: quote, quoteUrl: quote, quoteUri: quote, @@ -746,6 +749,9 @@ export class ApRendererService { mediaType: 'text/x.misskeymarkdown', }, }), + contentMap: note.lang ? { + [note.lang]: content, + } : null, _misskey_quote: quote, quoteUrl: quote, quoteUri: quote, diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 624ead52fa..d2f5f124f2 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -25,6 +25,7 @@ import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { checkHttps } from '@/misc/check-https.js'; +import { langmap } from '@/misc/langmap.js'; import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { ApMfmService } from '../ApMfmService.js'; @@ -244,12 +245,21 @@ export class ApNoteService { let text: string | null = null; if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { text = note.source.content; + } else if (note.contentMap != null) { + const entry = Object.entries(note.contentMap)[0]; + text = this.apMfmService.htmlToMfm(entry[1], note.tag); } 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); } + let lang: string | null = null; + if (note.contentMap != null) { + const key = Object.keys(note.contentMap)[0].toLowerCase(); + lang = Object.keys(langmap).includes(key) ? key : null; + } + // vote if (reply && reply.hasPoll) { const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id }); @@ -290,6 +300,7 @@ export class ApNoteService { name: note.name, cw, text, + lang, localOnly: false, visibility, visibleUsers, @@ -452,12 +463,21 @@ export class ApNoteService { let text: string | null = null; if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { text = note.source.content; + } else if (note.contentMap != null) { + const entry = Object.entries(note.contentMap)[0]; + text = this.apMfmService.htmlToMfm(entry[1], note.tag); } 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); } + let lang: string | null = null; + if (note.contentMap != null) { + const key = Object.keys(note.contentMap)[0].toLowerCase(); + lang = Object.keys(langmap).includes(key) ? key : null; + } + // vote if (reply && reply.hasPoll) { const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id }); @@ -498,6 +518,7 @@ export class ApNoteService { name: note.name, cw, text, + lang, localOnly: false, visibility, visibleUsers, diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 716515840c..00deddd431 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -21,6 +21,7 @@ export interface IObject { inReplyTo?: any; replies?: ICollection; content?: string | null; + contentMap?: Obj | null; startTime?: Date; endTime?: Date; icon?: any; diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 86a8670f29..465b5c28c0 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -17,12 +17,12 @@ import { bindThis } from '@/decorators.js'; import { isNotNull } from '@/misc/is-not-null.js'; import { DebounceLoader } from '@/misc/loader.js'; import { IdService } from '@/core/IdService.js'; +import type { Config } from '@/config.js'; import type { OnModuleInit } from '@nestjs/common'; import type { CustomEmojiService } from '../CustomEmojiService.js'; import type { ReactionService } from '../ReactionService.js'; import type { UserEntityService } from './UserEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js'; -import type { Config } from '@/config.js'; @Injectable() export class NoteEntityService implements OnModuleInit { @@ -120,7 +120,7 @@ export class NoteEntityService implements OnModuleInit { followerId: meId, }, }); - + hide = !isFollowing; } else { // フォロワーかどうか @@ -373,6 +373,7 @@ export class NoteEntityService implements OnModuleInit { uri: note.uri ?? undefined, url: note.url ?? undefined, poll: note.hasPoll ? this.populatePoll(note, meId) : undefined, + lang: note.lang, ...(meId && Object.keys(note.reactions).length > 0 ? { myReaction: this.populateMyReaction(note, meId, options?._hint_), } : {}), diff --git a/packages/backend/src/misc/langmap.ts b/packages/backend/src/misc/langmap.ts index 5ff9338651..1d2ec28212 100644 --- a/packages/backend/src/misc/langmap.ts +++ b/packages/backend/src/misc/langmap.ts @@ -4,668 +4,384 @@ */ // TODO: sharedに置いてフロントエンドのと統合したい -export const langmap = { - 'ach': { - nativeName: 'Lwo', - }, - 'ady': { - nativeName: 'Адыгэбзэ', - }, - 'af': { +export const iso639Langs1 = { + af: { nativeName: 'Afrikaans', }, - 'af-NA': { - nativeName: 'Afrikaans (Namibia)', - }, - 'af-ZA': { - nativeName: 'Afrikaans (South Africa)', - }, - 'ak': { + ak: { nativeName: 'Tɕɥi', }, - 'ar': { + ar: { nativeName: 'العربية', + rtl: true, }, - 'ar-AR': { - nativeName: 'العربية', - }, - 'ar-MA': { - nativeName: 'العربية', - }, - 'ar-SA': { - nativeName: 'العربية (السعودية)', - }, - 'ay-BO': { + ay: { nativeName: 'Aymar aru', }, - 'az': { + az: { nativeName: 'Azərbaycan dili', }, - 'az-AZ': { - nativeName: 'Azərbaycan dili', - }, - 'be-BY': { + be: { nativeName: 'Беларуская', }, - 'bg': { + bg: { nativeName: 'Български', }, - 'bg-BG': { - nativeName: 'Български', - }, - 'bn': { + bn: { nativeName: 'বাংলা', }, - 'bn-IN': { - nativeName: 'বাংলা (ভারত)', - }, - 'bn-BD': { - nativeName: 'বাংলা(বাংলাদেশ)', - }, - 'br': { + br: { nativeName: 'Brezhoneg', }, - 'bs-BA': { + bs: { nativeName: 'Bosanski', }, - 'ca': { + ca: { nativeName: 'Català', }, - 'ca-ES': { - nativeName: 'Català', - }, - 'cak': { - nativeName: 'Maya Kaqchikel', - }, - 'ck-US': { - nativeName: 'ᏣᎳᎩ (tsalagi)', - }, - 'cs': { + cs: { nativeName: 'Čeština', }, - 'cs-CZ': { - nativeName: 'Čeština', - }, - 'cy': { + cy: { nativeName: 'Cymraeg', }, - 'cy-GB': { - nativeName: 'Cymraeg', - }, - 'da': { + da: { nativeName: 'Dansk', }, - 'da-DK': { - nativeName: 'Dansk', - }, - 'de': { + de: { nativeName: 'Deutsch', }, - 'de-AT': { - nativeName: 'Deutsch (Österreich)', - }, - 'de-DE': { - nativeName: 'Deutsch (Deutschland)', - }, - 'de-CH': { - nativeName: 'Deutsch (Schweiz)', - }, - 'dsb': { - nativeName: 'Dolnoserbšćina', - }, - 'el': { + el: { nativeName: 'Ελληνικά', }, - 'el-GR': { - nativeName: 'Ελληνικά', - }, - 'en': { + en: { nativeName: 'English', }, - 'en-GB': { - nativeName: 'English (UK)', - }, - 'en-AU': { - nativeName: 'English (Australia)', - }, - 'en-CA': { - nativeName: 'English (Canada)', - }, - 'en-IE': { - nativeName: 'English (Ireland)', - }, - 'en-IN': { - nativeName: 'English (India)', - }, - 'en-PI': { - nativeName: 'English (Pirate)', - }, - 'en-SG': { - nativeName: 'English (Singapore)', - }, - 'en-UD': { - nativeName: 'English (Upside Down)', - }, - 'en-US': { - nativeName: 'English (US)', - }, - 'en-ZA': { - nativeName: 'English (South Africa)', - }, - 'en@pirate': { - nativeName: 'English (Pirate)', - }, - 'eo': { + eo: { nativeName: 'Esperanto', }, - 'eo-EO': { - nativeName: 'Esperanto', - }, - 'es': { + es: { nativeName: 'Español', }, - 'es-AR': { - nativeName: 'Español (Argentine)', - }, - 'es-419': { - nativeName: 'Español (Latinoamérica)', - }, - 'es-CL': { - nativeName: 'Español (Chile)', - }, - 'es-CO': { - nativeName: 'Español (Colombia)', - }, - 'es-EC': { - nativeName: 'Español (Ecuador)', - }, - 'es-ES': { - nativeName: 'Español (España)', - }, - 'es-LA': { - nativeName: 'Español (Latinoamérica)', - }, - 'es-NI': { - nativeName: 'Español (Nicaragua)', - }, - 'es-MX': { - nativeName: 'Español (México)', - }, - 'es-US': { - nativeName: 'Español (Estados Unidos)', - }, - 'es-VE': { - nativeName: 'Español (Venezuela)', - }, - 'et': { + et: { nativeName: 'eesti keel', }, - 'et-EE': { - nativeName: 'Eesti (Estonia)', - }, - 'eu': { + eu: { nativeName: 'Euskara', }, - 'eu-ES': { - nativeName: 'Euskara', - }, - 'fa': { + fa: { nativeName: 'فارسی', + rtl: true, }, - 'fa-IR': { - nativeName: 'فارسی', - }, - 'fb-LT': { - nativeName: 'Leet Speak', - }, - 'ff': { + ff: { nativeName: 'Fulah', }, - 'fi': { + fi: { nativeName: 'Suomi', }, - 'fi-FI': { - nativeName: 'Suomi', - }, - 'fo': { + fo: { nativeName: 'Føroyskt', }, - 'fo-FO': { - nativeName: 'Føroyskt (Færeyjar)', - }, - 'fr': { + fr: { nativeName: 'Français', }, - 'fr-CA': { - nativeName: 'Français (Canada)', - }, - 'fr-FR': { - nativeName: 'Français (France)', - }, - 'fr-BE': { - nativeName: 'Français (Belgique)', - }, - 'fr-CH': { - nativeName: 'Français (Suisse)', - }, - 'fy-NL': { + fy: { nativeName: 'Frysk', }, - 'ga': { + ga: { nativeName: 'Gaeilge', }, - 'ga-IE': { - nativeName: 'Gaeilge', - }, - 'gd': { + gd: { nativeName: 'Gàidhlig', }, - 'gl': { + gl: { nativeName: 'Galego', }, - 'gl-ES': { - nativeName: 'Galego', - }, - 'gn-PY': { + gn: { nativeName: 'Avañe\'ẽ', }, - 'gu-IN': { + gu: { nativeName: 'ગુજરાતી', }, - 'gv': { + gv: { nativeName: 'Gaelg', }, - 'gx-GR': { - nativeName: 'Ἑλληνική ἀρχαία', - }, - 'he': { + he: { nativeName: 'עברית‏', + rtl: true, }, - 'he-IL': { - nativeName: 'עברית‏', - }, - 'hi': { + hi: { nativeName: 'हिन्दी', }, - 'hi-IN': { - nativeName: 'हिन्दी', - }, - 'hr': { + hr: { nativeName: 'Hrvatski', }, - 'hr-HR': { - nativeName: 'Hrvatski', - }, - 'hsb': { - nativeName: 'Hornjoserbšćina', - }, - 'ht': { + ht: { nativeName: 'Kreyòl', }, - 'hu': { + hu: { nativeName: 'Magyar', }, - 'hu-HU': { - nativeName: 'Magyar', - }, - 'hy': { + hy: { nativeName: 'Հայերեն', }, - 'hy-AM': { - nativeName: 'Հայերեն (Հայաստան)', - }, - 'id': { + id: { nativeName: 'Bahasa Indonesia', }, - 'id-ID': { - nativeName: 'Bahasa Indonesia', - }, - 'is': { + is: { nativeName: 'Íslenska', }, - 'is-IS': { - nativeName: 'Íslenska (Iceland)', - }, - 'it': { + it: { nativeName: 'Italiano', }, - 'it-IT': { - nativeName: 'Italiano', - }, - 'ja': { + ja: { nativeName: '日本語', }, - 'ja-JP': { - nativeName: '日本語 (日本)', - }, - 'jv-ID': { + jv: { nativeName: 'Basa Jawa', }, - 'ka-GE': { + ka: { nativeName: 'ქართული', }, - 'kk-KZ': { + kk: { nativeName: 'Қазақша', }, - 'km': { - nativeName: 'ភាសាខ្មែរ', - }, - 'kl': { + kl: { nativeName: 'kalaallisut', }, - 'km-KH': { + km: { nativeName: 'ភាសាខ្មែរ', }, - 'kab': { - nativeName: 'Taqbaylit', - }, - 'kn': { + kn: { nativeName: 'ಕನ್ನಡ', }, - 'kn-IN': { - nativeName: 'ಕನ್ನಡ (India)', - }, - 'ko': { + ko: { nativeName: '한국어', }, - 'ko-KR': { - nativeName: '한국어 (한국)', - }, - 'ku-TR': { + ku: { nativeName: 'Kurdî', }, - 'kw': { + kw: { nativeName: 'Kernewek', }, - 'la': { + la: { nativeName: 'Latin', }, - 'la-VA': { - nativeName: 'Latin', - }, - 'lb': { + lb: { nativeName: 'Lëtzebuergesch', }, - 'li-NL': { + li: { nativeName: 'Lèmbörgs', }, - 'lt': { + lt: { nativeName: 'Lietuvių', }, - 'lt-LT': { - nativeName: 'Lietuvių', - }, - 'lv': { + lv: { nativeName: 'Latviešu', }, - 'lv-LV': { - nativeName: 'Latviešu', - }, - 'mai': { - nativeName: 'मैथिली, মৈথিলী', - }, - 'mg-MG': { + mg: { nativeName: 'Malagasy', }, - 'mk': { + mk: { nativeName: 'Македонски', }, - 'mk-MK': { - nativeName: 'Македонски (Македонски)', - }, - 'ml': { + ml: { nativeName: 'മലയാളം', }, - 'ml-IN': { - nativeName: 'മലയാളം', - }, - 'mn-MN': { + mn: { nativeName: 'Монгол', }, - 'mr': { + mr: { nativeName: 'मराठी', }, - 'mr-IN': { - nativeName: 'मराठी', - }, - 'ms': { + ms: { nativeName: 'Bahasa Melayu', }, - 'ms-MY': { - nativeName: 'Bahasa Melayu', - }, - 'mt': { + mt: { nativeName: 'Malti', }, - 'mt-MT': { - nativeName: 'Malti', - }, - 'my': { + my: { nativeName: 'ဗမာစကာ', }, - 'no': { + no: { nativeName: 'Norsk', }, - 'nb': { + nb: { nativeName: 'Norsk (bokmål)', }, - 'nb-NO': { - nativeName: 'Norsk (bokmål)', - }, - 'ne': { + ne: { nativeName: 'नेपाली', }, - 'ne-NP': { - nativeName: 'नेपाली', - }, - 'nl': { + nl: { nativeName: 'Nederlands', }, - 'nl-BE': { - nativeName: 'Nederlands (België)', - }, - 'nl-NL': { - nativeName: 'Nederlands (Nederland)', - }, - 'nn-NO': { + nn: { nativeName: 'Norsk (nynorsk)', }, - 'oc': { + oc: { nativeName: 'Occitan', }, - 'or-IN': { + or: { nativeName: 'ଓଡ଼ିଆ', }, - 'pa': { + pa: { nativeName: 'ਪੰਜਾਬੀ', }, - 'pa-IN': { - nativeName: 'ਪੰਜਾਬੀ (ਭਾਰਤ ਨੂੰ)', - }, - 'pl': { + pl: { nativeName: 'Polski', }, - 'pl-PL': { - nativeName: 'Polski', - }, - 'ps-AF': { + ps: { nativeName: 'پښتو', + rtl: true, }, - 'pt': { + pt: { nativeName: 'Português', }, - 'pt-BR': { - nativeName: 'Português (Brasil)', - }, - 'pt-PT': { - nativeName: 'Português (Portugal)', - }, - 'qu-PE': { + qu: { nativeName: 'Qhichwa', }, - 'rm-CH': { + rm: { nativeName: 'Rumantsch', }, - 'ro': { + ro: { nativeName: 'Română', }, - 'ro-RO': { - nativeName: 'Română', - }, - 'ru': { + ru: { nativeName: 'Русский', }, - 'ru-RU': { - nativeName: 'Русский', - }, - 'sa-IN': { + sa: { nativeName: 'संस्कृतम्', }, - 'se-NO': { + se: { nativeName: 'Davvisámegiella', }, - 'sh': { + sh: { nativeName: 'српскохрватски', }, - 'si-LK': { + si: { nativeName: 'සිංහල', }, - 'sk': { + sk: { nativeName: 'Slovenčina', }, - 'sk-SK': { - nativeName: 'Slovenčina (Slovakia)', - }, - 'sl': { + sl: { nativeName: 'Slovenščina', }, - 'sl-SI': { - nativeName: 'Slovenščina', - }, - 'so-SO': { + so: { nativeName: 'Soomaaliga', }, - 'sq': { + sq: { nativeName: 'Shqip', }, - 'sq-AL': { - nativeName: 'Shqip', - }, - 'sr': { + sr: { nativeName: 'Српски', }, - 'sr-RS': { - nativeName: 'Српски (Serbia)', - }, - 'su': { + su: { nativeName: 'Basa Sunda', }, - 'sv': { + sv: { nativeName: 'Svenska', }, - 'sv-SE': { - nativeName: 'Svenska', - }, - 'sw': { + sw: { nativeName: 'Kiswahili', }, - 'sw-KE': { - nativeName: 'Kiswahili', - }, - 'ta': { + ta: { nativeName: 'தமிழ்', }, - 'ta-IN': { - nativeName: 'தமிழ்', - }, - 'te': { + te: { nativeName: 'తెలుగు', }, - 'te-IN': { - nativeName: 'తెలుగు', - }, - 'tg': { + tg: { nativeName: 'забо́ни тоҷикӣ́', }, - 'tg-TJ': { - nativeName: 'тоҷикӣ', - }, - 'th': { + th: { nativeName: 'ภาษาไทย', }, - 'th-TH': { - nativeName: 'ภาษาไทย (ประเทศไทย)', - }, - 'fil': { - nativeName: 'Filipino', - }, - 'tlh': { - nativeName: 'tlhIngan-Hol', - }, - 'tr': { + tr: { nativeName: 'Türkçe', }, - 'tr-TR': { - nativeName: 'Türkçe', - }, - 'tt-RU': { + tt: { nativeName: 'татарча', }, - 'uk': { + uk: { nativeName: 'Українська', }, - 'uk-UA': { - nativeName: 'Українська', - }, - 'ur': { + ur: { nativeName: 'اردو', + rtl: true, }, - 'ur-PK': { - nativeName: 'اردو', - }, - 'uz': { + uz: { nativeName: 'O\'zbek', }, - 'uz-UZ': { - nativeName: 'O\'zbek', - }, - 'vi': { + vi: { nativeName: 'Tiếng Việt', }, - 'vi-VN': { - nativeName: 'Tiếng Việt', - }, - 'xh-ZA': { + xh: { nativeName: 'isiXhosa', }, - 'yi': { + yi: { nativeName: 'ייִדיש', + rtl: true, }, - 'yi-DE': { - nativeName: 'ייִדיש (German)', - }, - 'zh': { + zh: { nativeName: '中文', }, - 'zh-Hans': { - nativeName: '中文简体', - }, - 'zh-Hant': { - nativeName: '中文繁體', - }, - 'zh-CN': { - nativeName: '中文(中国大陆)', - }, - 'zh-HK': { - nativeName: '中文(香港)', - }, - 'zh-SG': { - nativeName: '中文(新加坡)', - }, - 'zh-TW': { - nativeName: '中文(台灣)', - }, - 'zu-ZA': { + zu: { nativeName: 'isiZulu', }, }; + +export const iso639Langs3 = { + ach: { + nativeName: 'Lwo', + }, + ady: { + nativeName: 'Адыгэбзэ', + }, + cak: { + nativeName: 'Maya Kaqchikel', + }, + chr: { + nativeName: 'ᏣᎳᎩ (tsalagi)', + }, + dsb: { + nativeName: 'Dolnoserbšćina', + }, + fil: { + nativeName: 'Filipino', + }, + hsb: { + nativeName: 'Hornjoserbšćina', + }, + kab: { + nativeName: 'Taqbaylit', + }, + mai: { + nativeName: 'मैथिली, মৈথিলী', + }, + tlh: { + nativeName: 'tlhIngan-Hol', + }, + tok: { + nativeName: 'Toki Pona', + }, + yue: { + nativeName: '粵語', + }, + nan: { + nativeName: '閩南語', + }, +}; + +export const langmapNoRegion = Object.assign({}, iso639Langs1, iso639Langs3); + +export const iso639Regional = { + 'zh-hans': { + nativeName: '中文(简体)', + }, + 'zh-hant': { + nativeName: '中文(繁體)', + }, +}; + +export const langmap = Object.assign({}, langmapNoRegion, iso639Regional); diff --git a/packages/backend/src/models/Note.ts b/packages/backend/src/models/Note.ts index b11e2ec62b..c1cf45e5f5 100644 --- a/packages/backend/src/models/Note.ts +++ b/packages/backend/src/models/Note.ts @@ -61,6 +61,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/NoteEdit.ts b/packages/backend/src/models/NoteEdit.ts index 9ec989dd50..942ba6a0f3 100644 --- a/packages/backend/src/models/NoteEdit.ts +++ b/packages/backend/src/models/NoteEdit.ts @@ -1,4 +1,4 @@ -import { Entity, JoinColumn, Column, ManyToOne, PrimaryColumn, Index } from "typeorm"; +import { Entity, JoinColumn, Column, ManyToOne, PrimaryColumn, Index } from 'typeorm'; import { id } from './util/id.js'; import { MiNote } from './Note.js'; import type { MiDriveFile } from './DriveFile.js'; @@ -11,46 +11,52 @@ export class NoteEdit { @Index() @Column({ ...id(), - comment: "The ID of note.", + comment: 'The ID of note.', }) - public noteId: MiNote["id"]; + public noteId: MiNote['id']; @ManyToOne((type) => MiNote, { - onDelete: "CASCADE", + onDelete: 'CASCADE', }) @JoinColumn() public note: MiNote | null; - @Column("text", { + @Column('text', { nullable: true, }) public oldText: string | null; - @Column("text", { + @Column('text', { nullable: true, }) public newText: string | null; - @Column("varchar", { + @Column('varchar', { length: 512, nullable: true, }) public cw: string | null; + @Column('varchar', { + length: 10, + nullable: true, + }) + public lang: string | null; + @Column({ ...id(), array: true, - default: "{}", + default: '{}', }) - public fileIds: MiDriveFile["id"][]; + public fileIds: MiDriveFile['id'][]; - @Column("timestamp with time zone", { - comment: "The updated date of the Note.", + @Column('timestamp with time zone', { + comment: 'The updated date of the Note.', }) public updatedAt: Date; - @Column("timestamp with time zone", { - comment: "The old date from before the edit", + @Column('timestamp with time zone', { + comment: 'The old date from before the edit', }) public oldDate: Date; } diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts index bb4ccc7ee4..766ff411fb 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/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index aff3867091..3c6714235d 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -20,6 +20,7 @@ import { isPureRenote } from '@/misc/is-pure-renote.js'; import { MetaService } from '@/core/MetaService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { langmap } from '@/misc/langmap.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -136,6 +137,7 @@ export const paramDef = { visibleUserIds: { type: 'array', uniqueItems: true, items: { type: 'string', format: 'misskey:id', } }, + lang: { type: 'string', enum: Object.keys(langmap), nullable: true, maxLength: 10 }, cw: { type: 'string', nullable: true, minLength: 1, maxLength: 500 }, localOnly: { type: 'boolean', default: false }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, @@ -370,6 +372,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/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts index 590853e9cd..e12c088e39 100644 --- a/packages/backend/src/server/api/endpoints/notes/edit.ts +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -12,6 +12,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEditService } from '@/core/NoteEditService.js'; import { DI } from '@/di-symbols.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { langmap } from '@/misc/langmap.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -164,6 +165,7 @@ export const paramDef = { format: 'misskey:id', }, }, + lang: { type: 'string', enum: Object.keys(langmap), nullable: true, maxLength: 10 }, cw: { type: 'string', nullable: true, minLength: 1, maxLength: 500 }, localOnly: { type: 'boolean', default: false }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, @@ -301,7 +303,7 @@ export default class extends Endpoint { // eslint- if (ps.renoteId === ps.editId) { throw new ApiError(meta.errors.cannotQuoteCurrentPost); } - + if (ps.renoteId != null) { // Fetch renote to note renote = await this.notesRepository.findOneBy({ id: ps.renoteId }); @@ -378,6 +380,13 @@ export default class extends Endpoint { // eslint- } } + if (ps.lang) { + if (!Object.keys(langmap).includes(ps.lang.toLowerCase())) throw new Error('invalid param'); + ps.lang = ps.lang.toLowerCase(); + } else { + ps.lang = null; + } + let channel: MiChannel | null = null; if (ps.channelId != null) { channel = await this.channelsRepository.findOneBy({ id: ps.channelId, isArchived: false }); @@ -396,6 +405,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/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index aad2f80871..682a9139ee 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -32,6 +32,10 @@ SPDX-License-Identifier: AGPL-3.0-only {{ channel.name }} + - +
- +
diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index 63860c3eb3..9293dd2310 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -9,6 +9,12 @@ openRemoteProfile: "Open remote profile" trustedLinkUrlPatterns: "Link to external site warning exclusion list" trustedLinkUrlPatternsDescription: "Separate with spaces for an AND condition or with line breaks for an OR condition. Using surrounding keywords with slashes will turn them into a regular expression. If you write only the domain name, it will be a backward match." mutuals: "Mutuals" +isLocked: "Private account" +isAdmin: "Administrator" +isBot: "Bot user" +open: "Open" +emailDestination: "Destination address" +date: "Date" renote: "Boost" unrenote: "Remove boost" renoted: "Boosted." @@ -383,4 +389,10 @@ _externalNavigationWarning: title: "Navigate to an external site" description: "Leave {host} and go to an external site" trustThisDomain: "Trust this domain on this device in the future" + remoteFollowersWarning: "Remote followers may have incomplete or outdated activity" + +_auth: + allowed: "Allowed" +_announcement: + new: "New" From b26b7a95704ed5f9ac2d100fda2eedf96d7b93f5 Mon Sep 17 00:00:00 2001 From: dakkar Date: Sun, 20 Oct 2024 20:53:36 +0100 Subject: [PATCH 664/867] re-add `warnExternalUrl` --- sharkey-locales/en-US.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index 9293dd2310..163fd0b0ae 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -155,6 +155,7 @@ showNonPublicNotes: "Show non-public" allowClickingNotifications: "Allow clicking on pop-up notifications" pinnedOnly: "Pinned" blockingYou: "Blocking you" +warnExternalUrl: "Show warning when opening external URLs" _delivery: stop: "Suspend delivery" resume: "Resume delivery" From f781c19df12bc4cdd40c0b1eb11aa9e8dbc8db90 Mon Sep 17 00:00:00 2001 From: dakkar Date: Tue, 22 Oct 2024 19:24:19 +0100 Subject: [PATCH 665/867] explicit licence in eslint files --- eslint/locale.js | 5 +++++ eslint/locale.test.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/eslint/locale.js b/eslint/locale.js index 1b19066ec7..dbb807b714 100644 --- a/eslint/locale.js +++ b/eslint/locale.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: dakkar and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only +*/ + /* This is a ESLint rule to report use of the `i18n.ts` and `i18n.tsx` * objects that reference translation items that don't actually exist * in the lexicon (the `locale/` files) diff --git a/eslint/locale.test.js b/eslint/locale.test.js index 1c0727b400..2b69672d27 100644 --- a/eslint/locale.test.js +++ b/eslint/locale.test.js @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: dakkar and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only +*/ + const {RuleTester} = require("eslint"); const localeRule = require("./locale"); From e05420a92dc6307668727f78e8dc439d5283687b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A5=BA=E5=AD=90w=20=28Yumechi=29?= <35571479+eternal-flame-AD@users.noreply.github.com> Date: Tue, 22 Oct 2024 04:17:56 -0500 Subject: [PATCH 666/867] Merge commit from fork [ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236) Signed-off-by: eternal-flame-AD --- packages/backend/src/server/FileServerService.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index 19f082c61c..1a4d0cb48f 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -317,6 +317,12 @@ export class FileServerService { ); } + if (!request.headers['user-agent']) { + throw new StatusError('User-Agent is required', 400, 'User-Agent is required'); + } else if (request.headers['user-agent'].toLowerCase().indexOf('misskey/') !== -1) { + throw new StatusError('Refusing to proxy a request from another proxy', 403, 'Proxy is recursive'); + } + // Create temp file const file = await this.getStreamAndTypeFromUrl(url); if (file === '404') { From 67f977f4ff31259dd33ea5d871852eec65c9e6f9 Mon Sep 17 00:00:00 2001 From: Lhc_fl Date: Wed, 23 Oct 2024 23:13:57 +0800 Subject: [PATCH 667/867] fix: return getfromdb when FanoutTimeline is not enabled --- .../src/server/api/endpoints/notes/user-list-timeline.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index 6c7185c9eb..87f9b322a6 100644 --- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts @@ -112,7 +112,7 @@ export default class extends Endpoint { // eslint- this.activeUsersChart.read(me); - await this.noteEntityService.packMany(timeline, me); + return await this.noteEntityService.packMany(timeline, me); } const timeline = await this.fanoutTimelineEndpointService.timeline({ From d52f4748f27445e47681310dd579f91acf279687 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Thu, 24 Oct 2024 05:36:26 +0200 Subject: [PATCH 668/867] fix: send MFM payload for notes that don't contain advanced MFM (resolves #647) --- .../src/core/activitypub/ApRendererService.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 9e4ccc7019..0c68d9a067 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -420,7 +420,7 @@ export class ApRendererService { const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; - const { content, noMisskeyContent } = this.apMfmService.getNoteHtml(note, apAppend); + const { content } = this.apMfmService.getNoteHtml(note, apAppend); const emojis = await this.getEmojis(note.emojis); const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); @@ -450,13 +450,11 @@ export class ApRendererService { attributedTo, summary: summary ?? undefined, content: content ?? undefined, - ...(noMisskeyContent ? {} : { - _misskey_content: text, - source: { - content: text, - mediaType: 'text/x.misskeymarkdown', - }, - }), + _misskey_content: text, + source: { + content: text, + mediaType: 'text/x.misskeymarkdown', + }, _misskey_quote: quote, quoteUrl: quote, quoteUri: quote, From 724aff6e4ea96d7645868f3c3bea7f891d8e0956 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Thu, 24 Oct 2024 05:43:41 +0200 Subject: [PATCH 669/867] fix: make sure outgoing remote mentions get resolved correctly if referenced with non-canonical casing (resolves #646) --- packages/backend/src/core/MfmService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 2055ea7f37..fe677a3b21 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -412,7 +412,7 @@ export class MfmService { mention: (node) => { const a = doc.createElement('a'); const { username, host, acct } = node.props; - const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host); + const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase()); a.setAttribute('href', remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${this.config.url}/${acct}`); a.className = 'u-url mention'; a.textContent = acct; From aae7fff4949cf4b042cf145497d1e1e5a7a76dbf Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Thu, 24 Oct 2024 05:51:30 +0200 Subject: [PATCH 670/867] fix: make sure mentions of local users get rendered correctly during AP delivery (resolves #645) --- packages/backend/src/core/MfmService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/MfmService.ts b/packages/backend/src/core/MfmService.ts index 2055ea7f37..2200aeecea 100644 --- a/packages/backend/src/core/MfmService.ts +++ b/packages/backend/src/core/MfmService.ts @@ -413,7 +413,9 @@ export class MfmService { const a = doc.createElement('a'); const { username, host, acct } = node.props; const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host); - a.setAttribute('href', remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${this.config.url}/${acct}`); + a.setAttribute('href', remoteUserInfo + ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) + : `${this.config.url}/${acct.endsWith(`@${this.config.url}`) ? acct.substring(0, acct.length - this.config.url.length - 1) : acct}`); a.className = 'u-url mention'; a.textContent = acct; return a; From d7ffc0be62bfb52920c91714f7b03ffeb736f992 Mon Sep 17 00:00:00 2001 From: dakkar Date: Thu, 24 Oct 2024 13:47:54 +0100 Subject: [PATCH 671/867] MR !710 had missed `renderUpNote` --- .../src/core/activitypub/ApRendererService.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 0c68d9a067..42ee5bc58a 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -711,7 +711,7 @@ export class ApRendererService { const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; - const { content, noMisskeyContent } = this.apMfmService.getNoteHtml(note, apAppend); + const { content } = this.apMfmService.getNoteHtml(note, apAppend); const emojis = await this.getEmojis(note.emojis); const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); @@ -742,13 +742,11 @@ export class ApRendererService { summary: summary ?? undefined, content: content ?? undefined, updated: note.updatedAt?.toISOString(), - ...(noMisskeyContent ? {} : { - _misskey_content: text, - source: { - content: text, - mediaType: 'text/x.misskeymarkdown', - }, - }), + _misskey_content: text, + source: { + content: text, + mediaType: 'text/x.misskeymarkdown', + }, _misskey_quote: quote, quoteUrl: quote, quoteUri: quote, From 991995673d41cb85ca919d5f9a0bd9add81b270a Mon Sep 17 00:00:00 2001 From: dakkar Date: Thu, 24 Oct 2024 21:40:34 +0100 Subject: [PATCH 672/867] remove stripes from posting preview --- packages/frontend/src/components/MkPostForm.vue | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index f538340920..4a03234e90 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -1259,14 +1259,6 @@ defineExpose({ background-size: auto auto; } -html[data-color-scheme=dark] .preview { - background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, #0004 5px, #0004 10px); -} - -html[data-color-scheme=light] .preview { - background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, #00000005 5px, #00000005 10px); -} - .targetNote { padding: 0 20px 16px 20px; } From 524ddb96770690455b82522104a543c5b0b1f3b3 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 26 Oct 2024 08:57:26 -0400 Subject: [PATCH 673/867] fix race conditions in check_connect.js --- packages/backend/scripts/check_connect.js | 56 ++++++++++++++++------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/packages/backend/scripts/check_connect.js b/packages/backend/scripts/check_connect.js index 47be1c90d4..f33a450325 100644 --- a/packages/backend/scripts/check_connect.js +++ b/packages/backend/scripts/check_connect.js @@ -12,27 +12,49 @@ const config = loadConfig(); // createPostgresDataSource handels primaries and replicas automatically. // usually, it only opens connections first use, so we force it using // .initialize() -createPostgresDataSource(config) - .initialize() - .then(c => { c.destroy() }) - .catch(e => { throw e }); - +async function connectToPostgres(){ + const source = createPostgresDataSource(config); + await source.initialize(); + await source.destroy(); +} // Connect to all redis servers -function connectToRedis(redisOptions) { - const redis = new Redis(redisOptions); - redis.on('connect', () => redis.disconnect()); - redis.on('error', (e) => { - throw e; +async function connectToRedis(redisOptions) { + return await new Promise(async (resolve, reject) => { + const redis = new Redis({ + ...redisOptions, + lazyConnect: true, + reconnectOnError: false, + showFriendlyErrorStack: true, + }); + redis.on('error', e => reject(e)); + + try { + await redis.connect(); + resolve(); + + } catch (e) { + reject(e); + + } finally { + redis.disconnect(false); + } }); } // If not all of these are defined, the default one gets reused. // so we use a Set to only try connecting once to each **uniq** redis. -(new Set([ - config.redis, - config.redisForPubsub, - config.redisForJobQueue, - config.redisForTimelines, - config.redisForReactions, -])).forEach(connectToRedis); +const promises = Array + .from(new Set([ + config.redis, + config.redisForPubsub, + config.redisForJobQueue, + config.redisForTimelines, + config.redisForReactions, + ])) + .map(connectToRedis) + .concat([ + connectToPostgres() + ]); + +await Promise.allSettled(promises); From 560ee43dcf2b76cce4b69a449fcd8b9601b7d68d Mon Sep 17 00:00:00 2001 From: Hazel K Date: Mon, 7 Oct 2024 21:03:31 -0400 Subject: [PATCH 674/867] separate character limits for local and remote notes --- .config/ci.yml | 8 ++++++- .config/docker_example.yml | 8 ++++++- .config/example.yml | 8 ++++++- .../1728348353115-soft-limit-drive-comment.js | 16 +++++++++++++ packages/backend/src/config.ts | 11 ++++++++- packages/backend/src/const.ts | 19 --------------- .../backend/src/core/NoteCreateService.ts | 13 +++++++---- packages/backend/src/core/NoteEditService.ts | 13 +++++++---- .../core/activitypub/models/ApImageService.ts | 6 +++-- .../src/core/entities/MetaEntityService.ts | 3 +++ packages/backend/src/models/DriveFile.ts | 4 +--- .../backend/src/models/json-schema/meta.ts | 12 ++++++++++ .../src/server/NodeinfoServerService.ts | 3 +++ .../api/endpoints/drive/files/create.ts | 17 ++++++++++++-- .../api/endpoints/drive/files/update.ts | 16 +++++++++++-- .../endpoints/drive/files/upload-from-url.ts | 23 ++++++++++++++++--- .../src/server/api/endpoints/notes/create.ts | 2 +- packages/backend/test/e2e/note.ts | 4 +++- packages/misskey-js/src/autogen/types.ts | 3 +++ 19 files changed, 142 insertions(+), 47 deletions(-) create mode 100644 packages/backend/migration/1728348353115-soft-limit-drive-comment.js diff --git a/.config/ci.yml b/.config/ci.yml index 19ffe18d2c..ab6bbd5773 100644 --- a/.config/ci.yml +++ b/.config/ci.yml @@ -167,8 +167,14 @@ id: 'aidx' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 -# Amount of characters that can be used when writing notes (maximum: 100000, minimum: 1) +# Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1) maxNoteLength: 3000 +# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1) +maxRemoteNoteLength: 100000 +# Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) +maxAltTextLength: 20000 +# Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) +maxRemoteAltTextLength: 100000 # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 diff --git a/.config/docker_example.yml b/.config/docker_example.yml index 3a344e3089..8bd555dffc 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -250,8 +250,14 @@ id: 'aidx' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 -# Amount of characters that can be used when writing notes (maximum: 100000, minimum: 1) +# Amount of characters that can be used when writing notes. Longer notes will be rejected. (maximum: 100000, minimum: 1) maxNoteLength: 3000 +# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (maximum: 100000, minimum: 1) +maxRemoteNoteLength: 100000 +# Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) +maxAltTextLength: 20000 +# Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) +maxRemoteAltTextLength: 100000 # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 diff --git a/.config/example.yml b/.config/example.yml index b9086479ea..6b80dab747 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -261,8 +261,14 @@ id: 'aidx' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 -# Amount of characters that can be used when writing notes (maximum: 100000, minimum: 1) +# Amount of characters that can be used when writing notes. Longer notes will be rejected. (maximum: 100000, minimum: 1) maxNoteLength: 3000 +# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (maximum: 100000, minimum: 1) +maxRemoteNoteLength: 100000 +# Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) +maxAltTextLength: 20000 +# Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) +maxRemoteAltTextLength: 100000 # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 diff --git a/packages/backend/migration/1728348353115-soft-limit-drive-comment.js b/packages/backend/migration/1728348353115-soft-limit-drive-comment.js new file mode 100644 index 0000000000..4eb04432c2 --- /dev/null +++ b/packages/backend/migration/1728348353115-soft-limit-drive-comment.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class SoftLimitDriveComment1728348353115 { + name = 'SoftLimitDriveComment1728348353115' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "comment" TYPE text`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "drive_file" ALTER COLUMN "comment" TYPE varchar(100000)`); + } +} diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index c9411326a9..19f1d6c066 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -73,6 +73,9 @@ type Source = { maxFileSize?: number; maxNoteLength?: number; + maxRemoteNoteLength?: number; + maxAltTextLength?: number; + maxRemoteAltTextLength?: number; clusterLimit?: number; @@ -149,6 +152,9 @@ export type Config = { allowedPrivateNetworks: string[] | undefined; maxFileSize: number; maxNoteLength: number; + maxRemoteNoteLength: number; + maxAltTextLength: number; + maxRemoteAltTextLength: number; clusterLimit: number | undefined; id: string; outgoingAddress: string | undefined; @@ -301,6 +307,9 @@ export function loadConfig(): Config { allowedPrivateNetworks: config.allowedPrivateNetworks, maxFileSize: config.maxFileSize ?? 262144000, maxNoteLength: config.maxNoteLength ?? 3000, + maxRemoteNoteLength: config.maxRemoteNoteLength ?? 100000, + maxAltTextLength: config.maxAltTextLength ?? 20000, + maxRemoteAltTextLength: config.maxRemoteAltTextLength ?? 100000, clusterLimit: config.clusterLimit, outgoingAddress: config.outgoingAddress, outgoingAddressFamily: config.outgoingAddressFamily, @@ -475,7 +484,7 @@ function applyEnvOverrides(config: Source) { _apply_top(['sentryForBackend', 'enableNodeProfiling']); _apply_top([['clusterLimit', 'deliverJobConcurrency', 'inboxJobConcurrency', 'relashionshipJobConcurrency', 'deliverJobPerSec', 'inboxJobPerSec', 'relashionshipJobPerSec', 'deliverJobMaxAttempts', 'inboxJobMaxAttempts']]); _apply_top([['outgoingAddress', 'outgoingAddressFamily', 'proxy', 'proxySmtp', 'mediaProxy', 'proxyRemoteFiles', 'videoThumbnailGenerator']]); - _apply_top([['maxFileSize', 'maxNoteLength', 'pidFile']]); + _apply_top([['maxFileSize', 'maxNoteLength', 'maxRemoteNoteLength', 'maxAltTextLength', 'maxRemoteAltTextLength', 'pidFile']]); _apply_top(['import', ['downloadTimeout', 'maxFileSize']]); _apply_top([['signToActivityPubGet', 'checkActivityPubGetSignature']]); } diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts index 7cc22a5421..adb0a63ad7 100644 --- a/packages/backend/src/const.ts +++ b/packages/backend/src/const.ts @@ -3,30 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -export const MAX_NOTE_TEXT_LENGTH = 3000; - export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days export const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16; -//#region hard limits -// If you change DB_* values, you must also change the DB schema. - -/** - * Maximum note text length that can be stored in DB. - * Content Warnings are included in this limit. - * Surrogate pairs count as one - */ -export const DB_MAX_NOTE_TEXT_LENGTH = 100000; - -/** - * Maximum image description length that can be stored in DB. - * Surrogate pairs count as one - */ -export const DB_MAX_IMAGE_COMMENT_LENGTH = 100000; -//#endregion - // ブラウザで直接表示することを許可するファイルの種類のリスト // ここに含まれないものは application/octet-stream としてレスポンスされる // SVGはXSSを生むので許可しない diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 2a38ed80b7..25286992d6 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -45,7 +45,6 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ import { NoteReadService } from '@/core/NoteReadService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { bindThis } from '@/decorators.js'; -import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { RoleService } from '@/core/RoleService.js'; import { SearchService } from '@/core/SearchService.js'; import { FeaturedService } from '@/core/FeaturedService.js'; @@ -335,9 +334,13 @@ export class NoteCreateService implements OnApplicationShutdown { data.localOnly = true; } + const maxTextLength = user.host == null + ? this.config.maxNoteLength + : this.config.maxRemoteNoteLength; + if (data.text) { - if (data.text.length > DB_MAX_NOTE_TEXT_LENGTH) { - data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH); + if (data.text.length > maxTextLength) { + data.text = data.text.slice(0, maxTextLength); } data.text = data.text.trim(); if (data.text === '') { @@ -348,8 +351,8 @@ export class NoteCreateService implements OnApplicationShutdown { } if (data.cw) { - if (data.cw.length > DB_MAX_NOTE_TEXT_LENGTH) { - data.cw = data.cw.slice(0, DB_MAX_NOTE_TEXT_LENGTH); + if (data.cw.length > maxTextLength) { + data.cw = data.cw.slice(0, maxTextLength); } data.cw = data.cw.trim(); if (data.cw === '') { diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index df45595da9..b1dd32aef8 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -39,7 +39,6 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ import { NoteReadService } from '@/core/NoteReadService.js'; import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { bindThis } from '@/decorators.js'; -import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { RoleService } from '@/core/RoleService.js'; import { SearchService } from '@/core/SearchService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; @@ -365,9 +364,13 @@ export class NoteEditService implements OnApplicationShutdown { data.localOnly = true; } + const maxTextLength = user.host == null + ? this.config.maxNoteLength + : this.config.maxRemoteNoteLength; + if (data.text) { - if (data.text.length > DB_MAX_NOTE_TEXT_LENGTH) { - data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH); + if (data.text.length > maxTextLength) { + data.text = data.text.slice(0, maxTextLength); } data.text = data.text.trim(); if (data.text === '') { @@ -378,8 +381,8 @@ export class NoteEditService implements OnApplicationShutdown { } if (data.cw) { - if (data.cw.length > DB_MAX_NOTE_TEXT_LENGTH) { - data.cw = data.cw.slice(0, DB_MAX_NOTE_TEXT_LENGTH); + if (data.cw.length > maxTextLength) { + data.cw = data.cw.slice(0, maxTextLength); } data.cw = data.cw.trim(); if (data.cw === '') { diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index ba9f41ca24..259889d945 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -9,12 +9,12 @@ import type { DriveFilesRepository, MiMeta } from '@/models/_.js'; import type { MiRemoteUser } from '@/models/User.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import { truncate } from '@/misc/truncate.js'; -import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js'; import { DriveService } from '@/core/DriveService.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { checkHttps } from '@/misc/check-https.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; +import type { Config } from '@/config.js'; import { ApResolverService } from '../ApResolverService.js'; import { ApLoggerService } from '../ApLoggerService.js'; import { isDocument, type IObject } from '../type.js'; @@ -29,6 +29,8 @@ export class ApImageService { @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, + @Inject(DI.config) + private config: Config, private apResolverService: ApResolverService, private driveService: DriveService, @@ -83,7 +85,7 @@ export class ApImageService { uri: image.url, sensitive: !!(image.sensitive), isLink: !shouldBeCached, - comment: truncate(image.name ?? undefined, DB_MAX_IMAGE_COMMENT_LENGTH), + comment: truncate(image.name ?? undefined, this.config.maxRemoteAltTextLength), }); if (!file.isLink || file.url === image.url) return file; diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index 61655c9652..a94b3ae290 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -110,6 +110,9 @@ export class MetaEntityService { backgroundImageUrl: instance.backgroundImageUrl, logoImageUrl: instance.logoImageUrl, maxNoteTextLength: this.config.maxNoteLength, + maxRemoteNoteTextLength: this.config.maxRemoteNoteLength, + maxAltTextLength: this.config.maxAltTextLength, + maxRemoteAltTextLength: this.config.maxRemoteAltTextLength, defaultLightTheme, defaultDarkTheme, defaultLike: instance.defaultLike, diff --git a/packages/backend/src/models/DriveFile.ts b/packages/backend/src/models/DriveFile.ts index 7de4be4f96..12d7b31e00 100644 --- a/packages/backend/src/models/DriveFile.ts +++ b/packages/backend/src/models/DriveFile.ts @@ -4,7 +4,6 @@ */ import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm'; -import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js'; import { id } from './util/id.js'; import { MiUser } from './User.js'; import { MiDriveFolder } from './DriveFolder.js'; @@ -61,8 +60,7 @@ export class MiDriveFile { }) public size: number; - @Column('varchar', { - length: DB_MAX_IMAGE_COMMENT_LENGTH, + @Column('text', { nullable: true, comment: 'The comment of the DriveFile.', }) diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index 15e87648ff..dbc28a7bfd 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -168,6 +168,18 @@ export const packedMetaLiteSchema = { type: 'number', optional: false, nullable: false, }, + maxRemoteNoteTextLength: { + type: 'number', + optional: false, nullable: false, + }, + maxAltTextLength: { + type: 'number', + optional: false, nullable: false, + }, + maxRemoteAltTextLength: { + type: 'number', + optional: false, nullable: false, + }, ads: { type: 'array', optional: false, nullable: false, diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index bc8d3c0411..65a2a59a74 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -122,6 +122,9 @@ export class NodeinfoServerService { enableMcaptcha: meta.enableMcaptcha, enableTurnstile: meta.enableTurnstile, maxNoteTextLength: this.config.maxNoteLength, + maxRemoteNoteTextLength: this.config.maxRemoteNoteLength, + maxAltTextLength: this.config.maxAltTextLength, + maxRemoteAltTextLength: this.config.maxRemoteAltTextLength, enableEmail: meta.enableEmail, enableServiceWorker: meta.enableServiceWorker, proxyAccountName: proxyAccount ? proxyAccount.username : null, diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index 74eb4dded7..b8763af96a 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -5,11 +5,11 @@ import ms from 'ms'; import { Inject, Injectable } from '@nestjs/common'; -import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveService } from '@/core/DriveService.js'; +import type { Config } from '@/config.js'; import { ApiError } from '../../../error.js'; import { MiMeta } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; @@ -56,6 +56,12 @@ export const meta = { code: 'NO_FREE_SPACE', id: 'd08dbc37-a6a9-463a-8c47-96c32ab5f064', }, + + commentTooLong: { + message: 'Cannot upload the file because the comment exceeds the instance limit.', + code: 'COMMENT_TOO_LONG', + id: 'sj3hsm2l-s83j-4sk3-sk3j-sn3k2k4nsm3l', + }, }, } as const; @@ -64,7 +70,7 @@ export const paramDef = { properties: { folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, name: { type: 'string', nullable: true, default: null }, - comment: { type: 'string', nullable: true, maxLength: DB_MAX_IMAGE_COMMENT_LENGTH, default: null }, + comment: { type: 'string', nullable: true, default: null }, isSensitive: { type: 'boolean', default: false }, force: { type: 'boolean', default: false }, }, @@ -77,6 +83,9 @@ export default class extends Endpoint { // eslint- @Inject(DI.meta) private serverSettings: MiMeta, + @Inject(DI.config) + private config: Config, + private driveFileEntityService: DriveFileEntityService, private driveService: DriveService, ) { @@ -94,6 +103,10 @@ export default class extends Endpoint { // eslint- } } + if (ps.comment && ps.comment.length > this.config.maxAltTextLength) { + throw new ApiError(meta.errors.commentTooLong); + } + try { // Create file const driveFile = await this.driveService.addFile({ diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts index 5541018126..afad4ba0a6 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -9,8 +9,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { DriveService } from '@/core/DriveService.js'; +import type { Config } from '@/config.js'; import { ApiError } from '../../../error.js'; -import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js'; export const meta = { tags: ['drive'], @@ -51,6 +51,12 @@ export const meta = { code: 'RESTRICTED_BY_ROLE', id: '7f59dccb-f465-75ab-5cf4-3ce44e3282f7', }, + + commentTooLong: { + message: 'Cannot upload the file because the comment exceeds the instance limit.', + code: 'COMMENT_TOO_LONG', + id: 'sj3hsm2l-s83j-4sk3-sk3j-sn3k2k4nsm3l', + }, }, res: { type: 'object', @@ -66,7 +72,7 @@ export const paramDef = { folderId: { type: 'string', format: 'misskey:id', nullable: true }, name: { type: 'string' }, isSensitive: { type: 'boolean' }, - comment: { type: 'string', nullable: true, maxLength: DB_MAX_IMAGE_COMMENT_LENGTH }, + comment: { type: 'string', nullable: true }, }, required: ['fileId'], } as const; @@ -76,6 +82,8 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, + @Inject(DI.config) + private config: Config, private driveService: DriveService, private roleService: RoleService, @@ -90,6 +98,10 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.accessDenied); } + if (ps.comment && ps.comment.length > this.config.maxAltTextLength) { + throw new ApiError(meta.errors.commentTooLong); + } + let packedFile; try { diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts index 49d2e78d08..52a1c51b2c 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts @@ -4,12 +4,14 @@ */ import ms from 'ms'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveService } from '@/core/DriveService.js'; -import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js'; +import { ApiError } from '@/server/api/error.js'; +import { DI } from '@/di-symbols.js'; +import type { Config } from '@/config.js'; export const meta = { tags: ['drive'], @@ -26,6 +28,14 @@ export const meta = { prohibitMoved: true, kind: 'write:drive', + + errors: { + commentTooLong: { + message: 'Cannot upload the file because the comment exceeds the instance limit.', + code: 'COMMENT_TOO_LONG', + id: 'sj3hsm2l-s83j-4sk3-sk3j-sn3k2k4nsm3l', + }, + }, } as const; export const paramDef = { @@ -34,7 +44,7 @@ export const paramDef = { url: { type: 'string' }, folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, isSensitive: { type: 'boolean', default: false }, - comment: { type: 'string', nullable: true, maxLength: DB_MAX_IMAGE_COMMENT_LENGTH, default: null }, + comment: { type: 'string', nullable: true, default: null }, marker: { type: 'string', nullable: true, default: null }, force: { type: 'boolean', default: false }, }, @@ -44,11 +54,18 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.config) + private config: Config, + private driveFileEntityService: DriveFileEntityService, private driveService: DriveService, private globalEventService: GlobalEventService, ) { super(meta, paramDef, async (ps, user, _1, _2, _3, ip, headers) => { + if (ps.comment && ps.comment.length > this.config.maxAltTextLength) { + throw new ApiError(meta.errors.commentTooLong); + } + this.driveService.uploadFromUrl({ url: ps.url, user, folderId: ps.folderId, sensitive: ps.isSensitive, force: ps.force, comment: ps.comment, requestIp: ip, requestHeaders: headers }).then(file => { this.driveFileEntityService.pack(file, { self: true }).then(packedFile => { this.globalEventService.publishMainStream(user.id, 'urlUploadFinished', { diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 412491afaa..a66395f25c 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -147,7 +147,7 @@ export const paramDef = { visibleUserIds: { type: 'array', uniqueItems: true, items: { type: 'string', format: 'misskey:id', } }, - cw: { type: 'string', nullable: true, minLength: 1, maxLength: 500 }, + cw: { type: 'string', nullable: true, minLength: 1 }, localOnly: { type: 'boolean', default: false }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, noExtractMentions: { type: 'boolean', default: false }, diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts index 5937eb9b49..2a8ec8e7de 100644 --- a/packages/backend/test/e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -9,10 +9,12 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; import { MiNote } from '@/models/Note.js'; -import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { api, castAsError, initTestDb, post, role, signup, uploadFile, uploadUrl } from '../utils.js'; import type * as misskey from 'misskey-js'; +// TODO: these tests are probably wrong for depending on this, but that's a problem for later. +const MAX_NOTE_TEXT_LENGTH = 3000; + describe('Note', () => { let Notes: Repository; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index d41e7ab1c9..8167e22b7d 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -5153,6 +5153,9 @@ export type components = { iconUrl: string | null; sidebarLogoUrl: string | null; maxNoteTextLength: number; + maxRemoteNoteTextLength: number; + maxAltTextLength: number; + maxRemoteAltTextLength: number; ads: { /** * Format: id From 67185a5d5d7d7a6912e772c7ab6021e6da8b72fd Mon Sep 17 00:00:00 2001 From: Hazel K Date: Wed, 9 Oct 2024 16:17:52 -0400 Subject: [PATCH 675/867] fix UUID format --- packages/backend/src/server/api/endpoints/drive/files/create.ts | 2 +- packages/backend/src/server/api/endpoints/drive/files/update.ts | 2 +- .../src/server/api/endpoints/drive/files/upload-from-url.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index b8763af96a..f67ff6ddc4 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -60,7 +60,7 @@ export const meta = { commentTooLong: { message: 'Cannot upload the file because the comment exceeds the instance limit.', code: 'COMMENT_TOO_LONG', - id: 'sj3hsm2l-s83j-4sk3-sk3j-sn3k2k4nsm3l', + id: '333652d9-0826-40f5-a2c3-e2bedcbb9fe5', }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts index afad4ba0a6..1501abf9ce 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/update.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts @@ -55,7 +55,7 @@ export const meta = { commentTooLong: { message: 'Cannot upload the file because the comment exceeds the instance limit.', code: 'COMMENT_TOO_LONG', - id: 'sj3hsm2l-s83j-4sk3-sk3j-sn3k2k4nsm3l', + id: '333652d9-0826-40f5-a2c3-e2bedcbb9fe5', }, }, res: { diff --git a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts index 52a1c51b2c..e20d482e70 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/upload-from-url.ts @@ -33,7 +33,7 @@ export const meta = { commentTooLong: { message: 'Cannot upload the file because the comment exceeds the instance limit.', code: 'COMMENT_TOO_LONG', - id: 'sj3hsm2l-s83j-4sk3-sk3j-sn3k2k4nsm3l', + id: '333652d9-0826-40f5-a2c3-e2bedcbb9fe5', }, }, } as const; From a6befca845e52a346d81705bff40f8ba59159e11 Mon Sep 17 00:00:00 2001 From: Hazel K Date: Wed, 9 Oct 2024 16:20:03 -0400 Subject: [PATCH 676/867] clarify comment about MAX_NOTE_TEXT_LENGTH in tests --- packages/backend/test/e2e/note.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/test/e2e/note.ts b/packages/backend/test/e2e/note.ts index 2a8ec8e7de..7d91b60426 100644 --- a/packages/backend/test/e2e/note.ts +++ b/packages/backend/test/e2e/note.ts @@ -12,7 +12,7 @@ import { MiNote } from '@/models/Note.js'; import { api, castAsError, initTestDb, post, role, signup, uploadFile, uploadUrl } from '../utils.js'; import type * as misskey from 'misskey-js'; -// TODO: these tests are probably wrong for depending on this, but that's a problem for later. +// Important: this must match the value of maxNoteLength in .config/ci.yml! const MAX_NOTE_TEXT_LENGTH = 3000; describe('Note', () => { From 10d3d9f382ad00426854f4434180afdf84501bd8 Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sun, 20 Oct 2024 00:49:36 -0400 Subject: [PATCH 677/867] fix unit tests --- .config/ci.yml | 8 ++++---- .config/cypress-devcontainer.yml | 9 +++++++++ .config/docker_example.yml | 12 +++++------ .config/example.yml | 12 +++++------ .../server/api/endpoints/notes/create.test.ts | 20 ++++++++++--------- 5 files changed, 36 insertions(+), 25 deletions(-) diff --git a/.config/ci.yml b/.config/ci.yml index ab6bbd5773..f29ac392d9 100644 --- a/.config/ci.yml +++ b/.config/ci.yml @@ -168,13 +168,13 @@ id: 'aidx' #outgoingAddressFamily: ipv4 # Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1) -maxNoteLength: 3000 +#maxNoteLength: 3000 # Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1) -maxRemoteNoteLength: 100000 +#maxRemoteNoteLength: 100000 # Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) -maxAltTextLength: 20000 +#maxAltTextLength: 20000 # Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) -maxRemoteAltTextLength: 100000 +#maxRemoteAltTextLength: 100000 # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml index 91dce35155..66b5dceac8 100644 --- a/.config/cypress-devcontainer.yml +++ b/.config/cypress-devcontainer.yml @@ -179,6 +179,15 @@ id: 'aidx' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 +# Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1) +#maxNoteLength: 3000 +# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1) +#maxRemoteNoteLength: 100000 +# Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) +#maxAltTextLength: 20000 +# Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) +#maxRemoteAltTextLength: 100000 + # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 diff --git a/.config/docker_example.yml b/.config/docker_example.yml index 8bd555dffc..dd8ea1727a 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -250,14 +250,14 @@ id: 'aidx' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 -# Amount of characters that can be used when writing notes. Longer notes will be rejected. (maximum: 100000, minimum: 1) -maxNoteLength: 3000 -# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (maximum: 100000, minimum: 1) -maxRemoteNoteLength: 100000 +# Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1) +#maxNoteLength: 3000 +# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1) +#maxRemoteNoteLength: 100000 # Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) -maxAltTextLength: 20000 +#maxAltTextLength: 20000 # Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) -maxRemoteAltTextLength: 100000 +#maxRemoteAltTextLength: 100000 # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 diff --git a/.config/example.yml b/.config/example.yml index 6b80dab747..8794a25ffb 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -261,14 +261,14 @@ id: 'aidx' # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 -# Amount of characters that can be used when writing notes. Longer notes will be rejected. (maximum: 100000, minimum: 1) -maxNoteLength: 3000 -# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (maximum: 100000, minimum: 1) -maxRemoteNoteLength: 100000 +# Amount of characters that can be used when writing notes. Longer notes will be rejected. (minimum: 1) +#maxNoteLength: 3000 +# Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1) +#maxRemoteNoteLength: 100000 # Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) -maxAltTextLength: 20000 +#maxAltTextLength: 20000 # Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) -maxRemoteAltTextLength: 100000 +#maxRemoteAltTextLength: 100000 # Proxy for HTTP/HTTPS #proxy: http://127.0.0.1:3128 diff --git a/packages/backend/src/server/api/endpoints/notes/create.test.ts b/packages/backend/src/server/api/endpoints/notes/create.test.ts index f3d887bb20..18d80e867b 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.test.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.test.ts @@ -5,15 +5,12 @@ process.env.NODE_ENV = 'test'; -import { readFile } from 'node:fs/promises'; -import { fileURLToPath } from 'node:url'; -import { dirname } from 'node:path'; import { describe, test, expect } from '@jest/globals'; +import { loadConfig } from '@/config.js'; import { getValidator } from '../../../../../test/prelude/get-api-validator.js'; import { paramDef } from './create.js'; -const _filename = fileURLToPath(import.meta.url); -const _dirname = dirname(_filename); +const config = loadConfig(); const VALID = true; const INVALID = false; @@ -21,7 +18,12 @@ const INVALID = false; describe('api:notes/create', () => { describe('validation', () => { const v = getValidator(paramDef); - const tooLong = readFile(_dirname + '/../../../../../test/resources/misskey.svg', 'utf-8'); + const tooLong = (limit: number) => { + const arr: string[] = ['']; + arr.length = limit + 1; + arr.fill('a'); + return arr.join(''); + }; test('reject empty', () => { const valid = v({ }); @@ -71,8 +73,8 @@ describe('api:notes/create', () => { .toBe(INVALID); }); - test('over 500 characters cw', async () => { - expect(v({ text: 'Body', cw: await tooLong })) + test('over max characters cw', async () => { + expect(v({ text: '', cw: tooLong(config.maxNoteLength) })) .toBe(INVALID); }); }); @@ -220,7 +222,7 @@ describe('api:notes/create', () => { }); test('reject poll with too long choice', async () => { - expect(v({ poll: { choices: [await tooLong, '2'] } })) + expect(v({ poll: { choices: [tooLong(config.maxNoteLength), '2'] } })) .toBe(INVALID); }); From 01e98c75abc548bcd674526494cfc8ec0c7912ed Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 26 Oct 2024 10:04:23 -0400 Subject: [PATCH 678/867] add separate limits for CW length --- .config/ci.yml | 4 +++ .config/cypress-devcontainer.yml | 4 +++ .config/docker_example.yml | 4 +++ .config/example.yml | 4 +++ locales/index.d.ts | 36 +++++++++++++++++++ packages/backend/src/config.ts | 6 ++++ .../backend/src/core/NoteCreateService.ts | 8 +++-- packages/backend/src/core/NoteEditService.ts | 8 +++-- .../src/server/api/endpoints/notes/create.ts | 12 +++++-- .../src/server/api/endpoints/notes/edit.ts | 14 ++++++-- 10 files changed, 91 insertions(+), 9 deletions(-) diff --git a/.config/ci.yml b/.config/ci.yml index f29ac392d9..d20ede8d35 100644 --- a/.config/ci.yml +++ b/.config/ci.yml @@ -171,6 +171,10 @@ id: 'aidx' #maxNoteLength: 3000 # Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1) #maxRemoteNoteLength: 100000 +# Amount of characters that can be used when writing content warnings. Longer warnings will be rejected. (minimum: 1) +#maxCwLength: 500 +# Amount of characters that will be saved for remote content warnings. Longer warnings will be truncated to this length. (minimum: 1) +#maxRemoteCwLength: 5000 # Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) #maxAltTextLength: 20000 # Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml index 66b5dceac8..d8013a1c95 100644 --- a/.config/cypress-devcontainer.yml +++ b/.config/cypress-devcontainer.yml @@ -183,6 +183,10 @@ id: 'aidx' #maxNoteLength: 3000 # Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1) #maxRemoteNoteLength: 100000 +# Amount of characters that can be used when writing content warnings. Longer warnings will be rejected. (minimum: 1) +#maxCwLength: 500 +# Amount of characters that will be saved for remote content warnings. Longer warnings will be truncated to this length. (minimum: 1) +#maxRemoteCwLength: 5000 # Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) #maxAltTextLength: 20000 # Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) diff --git a/.config/docker_example.yml b/.config/docker_example.yml index dd8ea1727a..5fac3dc41e 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -254,6 +254,10 @@ id: 'aidx' #maxNoteLength: 3000 # Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1) #maxRemoteNoteLength: 100000 +# Amount of characters that can be used when writing content warnings. Longer warnings will be rejected. (minimum: 1) +#maxCwLength: 500 +# Amount of characters that will be saved for remote content warnings. Longer warnings will be truncated to this length. (minimum: 1) +#maxRemoteCwLength: 5000 # Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) #maxAltTextLength: 20000 # Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) diff --git a/.config/example.yml b/.config/example.yml index 8794a25ffb..0062b6670c 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -265,6 +265,10 @@ id: 'aidx' #maxNoteLength: 3000 # Amount of characters that will be saved for remote notes. Longer notes will be truncated to this length. (minimum: 1) #maxRemoteNoteLength: 100000 +# Amount of characters that can be used when writing content warnings. Longer warnings will be rejected. (minimum: 1) +#maxCwLength: 500 +# Amount of characters that will be saved for remote content warnings. Longer warnings will be truncated to this length. (minimum: 1) +#maxRemoteCwLength: 5000 # Amount of characters that can be used when writing media descriptions (alt text). Longer descriptions will be rejected. (minimum: 1) #maxAltTextLength: 20000 # Amount of characters that will be saved for remote media descriptions (alt text). Longer descriptions will be truncated to this length. (minimum: 1) diff --git a/locales/index.d.ts b/locales/index.d.ts index 535e88f7c7..d1cb1f97ea 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5353,6 +5353,10 @@ export interface Locale extends ILocale { * オンにすると、このお知らせは通知されず、既読にする必要もなくなります。 */ "silenceDescription": string; + /** + * New + */ + "new": string; }; "_initialAccountSetting": { /** @@ -8442,6 +8446,10 @@ export interface Locale extends ILocale { * アプリケーションにアクセス許可を与えるには、ログインが必要です。 */ "pleaseLogin": string; + /** + * Allowed + */ + "allowed": string; }; "_antennaSources": { /** @@ -10603,6 +10611,30 @@ export interface Locale extends ILocale { * Mutuals */ "mutuals": string; + /** + * Private account + */ + "isLocked": string; + /** + * Administrator + */ + "isAdmin": string; + /** + * Bot user + */ + "isBot": string; + /** + * Open + */ + "open": string; + /** + * Destination address + */ + "emailDestination": string; + /** + * Date + */ + "date": string; /** * Quoted. */ @@ -10964,6 +10996,10 @@ export interface Locale extends ILocale { * Blocking you */ "blockingYou": string; + /** + * Show warning when opening external URLs + */ + "warnExternalUrl": string; "_mfm": { /** * This is not a widespread feature, it may not display properly on most other fedi software, including other Misskey forks diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index 19f1d6c066..3dc49c7eb6 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -73,6 +73,8 @@ type Source = { maxFileSize?: number; maxNoteLength?: number; + maxCwLength?: number; + maxRemoteCwLength?: number; maxRemoteNoteLength?: number; maxAltTextLength?: number; maxRemoteAltTextLength?: number; @@ -153,6 +155,8 @@ export type Config = { maxFileSize: number; maxNoteLength: number; maxRemoteNoteLength: number; + maxCwLength: number; + maxRemoteCwLength: number; maxAltTextLength: number; maxRemoteAltTextLength: number; clusterLimit: number | undefined; @@ -308,6 +312,8 @@ export function loadConfig(): Config { maxFileSize: config.maxFileSize ?? 262144000, maxNoteLength: config.maxNoteLength ?? 3000, maxRemoteNoteLength: config.maxRemoteNoteLength ?? 100000, + maxCwLength: config.maxCwLength ?? 500, + maxRemoteCwLength: config.maxRemoteCwLength ?? 5000, maxAltTextLength: config.maxAltTextLength ?? 20000, maxRemoteAltTextLength: config.maxRemoteAltTextLength ?? 100000, clusterLimit: config.clusterLimit, diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 25286992d6..1bc4599a60 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -350,9 +350,13 @@ export class NoteCreateService implements OnApplicationShutdown { data.text = null; } + const maxCwLength = user.host == null + ? this.config.maxCwLength + : this.config.maxRemoteCwLength; + if (data.cw) { - if (data.cw.length > maxTextLength) { - data.cw = data.cw.slice(0, maxTextLength); + if (data.cw.length > maxCwLength) { + data.cw = data.cw.slice(0, maxCwLength); } data.cw = data.cw.trim(); if (data.cw === '') { diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index b1dd32aef8..d31958e5d4 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -380,9 +380,13 @@ export class NoteEditService implements OnApplicationShutdown { data.text = null; } + const maxCwLength = user.host == null + ? this.config.maxCwLength + : this.config.maxRemoteCwLength; + if (data.cw) { - if (data.cw.length > maxTextLength) { - data.cw = data.cw.slice(0, maxTextLength); + if (data.cw.length > maxCwLength) { + data.cw = data.cw.slice(0, maxCwLength); } data.cw = data.cw.trim(); if (data.cw === '') { diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index a66395f25c..d1cf0123dc 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -90,6 +90,12 @@ export const meta = { id: '3ac74a84-8fd5-4bb0-870f-01804f82ce16', }, + maxCwLength: { + message: 'You tried posting a content warning which is too long.', + code: 'MAX_CW_LENGTH', + id: '7004c478-bda3-4b4f-acb2-4316398c9d52', + }, + cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility: { message: 'You cannot reply to a specified visibility note with extended visibility.', code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY', @@ -250,10 +256,12 @@ export default class extends Endpoint { // eslint- private noteCreateService: NoteCreateService, ) { super(meta, paramDef, async (ps, me) => { - const contentLength = (ps.text?.length ?? 0) + (ps.cw?.length ?? 0); - if (contentLength > this.config.maxNoteLength) { + if (ps.text && ps.text.length > this.config.maxNoteLength) { throw new ApiError(meta.errors.maxLength); } + if (ps.cw && ps.cw.length > this.config.maxCwLength) { + throw new ApiError(meta.errors.maxCwLength); + } let visibleUsers: MiUser[] = []; if (ps.visibleUserIds) { diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts index b9be145caf..dc94c78e75 100644 --- a/packages/backend/src/server/api/endpoints/notes/edit.ts +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -86,6 +86,12 @@ export const meta = { id: '3ac74a84-8fd5-4bb0-870f-01804f82ce16', }, + maxCwLength: { + message: 'You tried posting a content warning which is too long.', + code: 'MAX_CW_LENGTH', + id: '7004c478-bda3-4b4f-acb2-4316398c9d52', + }, + cannotReplyToSpecifiedVisibilityNoteWithExtendedVisibility: { message: 'You cannot reply to a specified visibility note with extended visibility.', code: 'CANNOT_REPLY_TO_SPECIFIED_VISIBILITY_NOTE_WITH_EXTENDED_VISIBILITY', @@ -197,7 +203,7 @@ export const paramDef = { format: 'misskey:id', }, }, - cw: { type: 'string', nullable: true, minLength: 1, maxLength: 500 }, + cw: { type: 'string', nullable: true, minLength: 1 }, localOnly: { type: 'boolean', default: false }, reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, noExtractMentions: { type: 'boolean', default: false }, @@ -297,10 +303,12 @@ export default class extends Endpoint { // eslint- private noteEditService: NoteEditService, ) { super(meta, paramDef, async (ps, me) => { - const contentLength = (ps.text?.length ?? 0) + (ps.cw?.length ?? 0); - if (contentLength > this.config.maxNoteLength) { + if (ps.text && ps.text.length > this.config.maxNoteLength) { throw new ApiError(meta.errors.maxLength); } + if (ps.cw && ps.cw.length > this.config.maxCwLength) { + throw new ApiError(meta.errors.maxCwLength); + } let visibleUsers: MiUser[] = []; if (ps.visibleUserIds) { From c5d9bde43f0e74521538163e456df02adc42849b Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 26 Oct 2024 10:37:38 -0400 Subject: [PATCH 679/867] expose CW limit to frontend --- packages/backend/src/core/entities/MetaEntityService.ts | 2 ++ packages/backend/src/models/json-schema/meta.ts | 8 ++++++++ packages/backend/src/server/NodeinfoServerService.ts | 2 ++ packages/misskey-js/src/autogen/types.ts | 2 ++ 4 files changed, 14 insertions(+) diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index a94b3ae290..1c463fb0c9 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -111,6 +111,8 @@ export class MetaEntityService { logoImageUrl: instance.logoImageUrl, maxNoteTextLength: this.config.maxNoteLength, maxRemoteNoteTextLength: this.config.maxRemoteNoteLength, + maxCwLength: this.config.maxCwLength, + maxRemoteCwLength: this.config.maxRemoteCwLength, maxAltTextLength: this.config.maxAltTextLength, maxRemoteAltTextLength: this.config.maxRemoteAltTextLength, defaultLightTheme, diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts index dbc28a7bfd..92aff24b4b 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -172,6 +172,14 @@ export const packedMetaLiteSchema = { type: 'number', optional: false, nullable: false, }, + maxCwLength: { + type: 'number', + optional: false, nullable: false, + }, + maxRemoteCwLength: { + type: 'number', + optional: false, nullable: false, + }, maxAltTextLength: { type: 'number', optional: false, nullable: false, diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 65a2a59a74..9d33658756 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -123,6 +123,8 @@ export class NodeinfoServerService { enableTurnstile: meta.enableTurnstile, maxNoteTextLength: this.config.maxNoteLength, maxRemoteNoteTextLength: this.config.maxRemoteNoteLength, + maxCwLength: this.config.maxCwLength, + maxRemoteCwLength: this.config.maxRemoteCwLength, maxAltTextLength: this.config.maxAltTextLength, maxRemoteAltTextLength: this.config.maxRemoteAltTextLength, enableEmail: meta.enableEmail, diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 8167e22b7d..a1e697473d 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -5154,6 +5154,8 @@ export type components = { sidebarLogoUrl: string | null; maxNoteTextLength: number; maxRemoteNoteTextLength: number; + maxCwLength: number; + maxRemoteCwLength: number; maxAltTextLength: number; maxRemoteAltTextLength: number; ads: { From 726013057db3457ef1f57d66f4969bfa9e354e4c Mon Sep 17 00:00:00 2001 From: Hazelnoot Date: Sat, 26 Oct 2024 10:38:16 -0400 Subject: [PATCH 680/867] show separate counters for text limit and CW limit --- packages/frontend/src/components/MkPostForm.vue | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index f538340920..082b7d7983 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -65,7 +65,10 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.notSpecifiedMentionWarning }} - - +
+ +
{{ maxCwLength - cwLength }}
+