diff --git a/COPYING b/COPYING index 27aeb01ac9..5b35f69f26 100644 --- a/COPYING +++ b/COPYING @@ -28,6 +28,10 @@ Machine learning model for sensitive images by Infinite Red, Inc. License: MIT https://github.com/infinitered/nsfwjs/blob/master/LICENSE +Chiptune2.js by Simon Gündling +License: MIT +https://github.com/deskjet/chiptune2.js#license + Licenses for all softwares and software libraries installed via the Node Package Manager ("npm") can be found by running the following shell command in the root directory of this repository: pnpm licenses list diff --git a/docs/api-change.md b/docs/api-change.md index 4dc631f071..3dcdfc251e 100644 --- a/docs/api-change.md +++ b/docs/api-change.md @@ -4,6 +4,8 @@ Breaking changes are indecated by the :warning: icon. ## v1.0.5 (unreleased) +- Added `lang` parameter to `notes/create` and `notes/edit`. + ### dev11 - :warning: `notes/translate` now requires credentials. diff --git a/locales/en-US.yml b/locales/en-US.yml index 46242d5fc1..37de0e0acc 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -132,6 +132,7 @@ rememberNoteVisibility: "Remember post visibility settings" attachCancel: "Remove attachment" markAsSensitive: "Mark as NSFW" unmarkAsSensitive: "Unmark as NSFW" +clickToShowPatterns: "Click to show module patterns" enterFileName: "Enter filename" mute: "Mute" unmute: "Unmute" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 574afa851f..9df04022bb 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -120,6 +120,7 @@ rememberNoteVisibility: "Lembrar das configurações de visibilidade de notas" attachCancel: "Remover anexo" markAsSensitive: "Marcar como sensível" unmarkAsSensitive: "Desmarcar como sensível" +clickToShowPatterns: "Clique para mostrar os padrões do módulo" enterFileName: "Digite o nome do ficheiro" mute: "Silenciar" unmute: "Dessilenciar" diff --git a/package.json b/package.json index 721abfa93c..71aa1b2df8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firefish", - "version": "1.0.5-dev16", + "version": "1.0.5-dev17", "codename": "aqua", "repository": { "type": "git", diff --git a/packages/backend/migration/1695334243217-add-post-lang.js b/packages/backend/migration/1695334243217-add-post-lang.js new file mode 100644 index 0000000000..7e8618953a --- /dev/null +++ b/packages/backend/migration/1695334243217-add-post-lang.js @@ -0,0 +1,13 @@ +export class AddPostLang1695334243217 { + name = "AddPostLang1695334243217"; + + async up(queryRunner) { + await queryRunner.query( + `ALTER TABLE "note" ADD "lang" character varying(10)`, + ); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "note" DROP COLUMN "lang"`); + } +} diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts index 2a955ee521..6dddf1fff5 100644 --- a/packages/backend/src/const.ts +++ b/packages/backend/src/const.ts @@ -68,6 +68,15 @@ export const FILE_TYPE_BROWSERSAFE = [ "audio/x-flac", "audio/flac", "audio/vnd.wave", + + "audio/mod", + "audio/x-mod", + "audio/s3m", + "audio/x-s3m", + "audio/xm", + "audio/x-xm", + "audio/it", + "audio/x-it", ]; /* https://github.com/sindresorhus/file-type/blob/main/supported.js diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts index 21fe64e901..2b9de38cbe 100644 --- a/packages/backend/src/models/entities/note.ts +++ b/packages/backend/src/models/entities/note.ts @@ -66,6 +66,12 @@ export class Note { }) 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/repositories/note.ts b/packages/backend/src/models/repositories/note.ts index 62d99d3613..e7d9191a2c 100644 --- a/packages/backend/src/models/repositories/note.ts +++ b/packages/backend/src/models/repositories/note.ts @@ -300,8 +300,6 @@ export const NoteRepository = db.getRepository(Note).extend({ host, ); - const lang = - detectLanguage(`${note.cw ?? ""}\n${note.text ?? ""}`) ?? "unknown"; const reactionEmoji = await populateEmojis(reactionEmojiNames, host); const packed: Packed<"Note"> = await awaitAll({ id: note.id, @@ -376,7 +374,7 @@ export const NoteRepository = db.getRepository(Note).extend({ : undefined, } : {}), - lang: lang, + lang: note.lang, }); if (packed.user.isCat && packed.user.speakAsCat && packed.text) { diff --git a/packages/backend/src/remote/activitypub/models/note.ts b/packages/backend/src/remote/activitypub/models/note.ts index 3da3321a7d..392844da49 100644 --- a/packages/backend/src/remote/activitypub/models/note.ts +++ b/packages/backend/src/remote/activitypub/models/note.ts @@ -64,6 +64,7 @@ import { parseHomeTimeline, } from "@/db/scylla.js"; import type { Client } from "cassandra-driver"; +import { langmap } from "@/misc/langmap.js"; const logger = apLogger; @@ -316,11 +317,24 @@ export async function createNote( // Text parsing let text: string | null = null; + let lang: string | null = null; if ( note.source?.mediaType === "text/x.misskeymarkdown" && typeof note.source?.content === "string" ) { text = note.source.content; + if (note.contentMap != null) { + const key = Object.keys(note.contentMap)[0]; + lang = Object.keys(langmap).includes(key) + ? key.trim().split("-")[0].split("@")[0] + : null; + } + } else if (note.contentMap != null) { + const entry = Object.entries(note.contentMap)[0]; + lang = Object.keys(langmap).includes(entry[0]) + ? entry[0].trim().split("-")[0].split("@")[0] + : null; + text = htmlToMfm(entry[1], note.tag); } else if (typeof note.content === "string") { text = htmlToMfm(note.content, note.tag); } @@ -417,6 +431,7 @@ export async function createNote( name: note.name, cw, text, + lang, localOnly: false, visibility, visibleUsers, @@ -617,11 +632,24 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) { // Text parsing let text: string | null = null; + let lang: string | null = null; if ( post.source?.mediaType === "text/x.misskeymarkdown" && typeof post.source?.content === "string" ) { text = post.source.content; + if (post.contentMap != null) { + const key = Object.keys(post.contentMap)[0]; + lang = Object.keys(langmap).includes(key) + ? key.trim().split("-")[0].split("@")[0] + : null; + } + } else if (post.contentMap != null) { + const entry = Object.entries(post.contentMap)[0]; + lang = Object.keys(langmap).includes(entry[0]) + ? entry[0].trim().split("-")[0].split("@")[0] + : null; + text = htmlToMfm(entry[1], post.tag); } else if (typeof post.content === "string") { text = htmlToMfm(post.content, post.tag); } @@ -717,6 +745,9 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) { if (text && text !== note.text) { update.text = text; } + if (lang && lang !== note.lang) { + update.lang = lang; + } if (cw !== note.cw) { update.cw = cw ? cw : null; } diff --git a/packages/backend/src/remote/activitypub/renderer/note.ts b/packages/backend/src/remote/activitypub/renderer/note.ts index 40e3a102e6..1571e7ba58 100644 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ b/packages/backend/src/remote/activitypub/renderer/note.ts @@ -158,10 +158,12 @@ export default async function renderNote( }), ); - const lang = detectLanguage(text); - const contentMap = lang ? { - [lang]: content - } : null; + const lang = note.lang ?? detectLanguage(text); + const contentMap = lang + ? { + [lang]: content, + } + : null; const emojis = await getEmojis(note.emojis); const apemojis = emojis.map((emoji) => renderEmoji(emoji)); diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index 95022cdb6a..1ea9314117 100644 --- a/packages/backend/src/server/api/endpoints/notes/create.ts +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -109,6 +109,7 @@ export const paramDef = { }, }, text: { type: "string", maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true }, + lang: { type: "string", nullable: true, maxLength: 10 }, cw: { type: "string", nullable: true, maxLength: 100 }, localOnly: { type: "boolean", default: false }, noExtractMentions: { type: "boolean", default: false }, @@ -305,6 +306,7 @@ export default define(meta, paramDef, async (ps, user) => { } : 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 0f1db174e6..41b3e1bd5f 100644 --- a/packages/backend/src/server/api/endpoints/notes/edit.ts +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -1,5 +1,5 @@ import { In } from "typeorm"; -import create, { index } from "@/services/note/create.js"; +import { index } from "@/services/note/create.js"; import type { IRemoteUser, User } from "@/models/entities/user.js"; import { Users, @@ -46,6 +46,8 @@ import { parseHomeTimeline, } from "@/db/scylla.js"; import type { Client } from "cassandra-driver"; +import { detect as detectLanguage } from "tinyld"; +import { langmap } from "@/misc/langmap.js"; export const meta = { tags: ["notes"], @@ -180,6 +182,7 @@ export const paramDef = { }, }, text: { type: "string", maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true }, + lang: { type: "string", nullable: true, maxLength: 10 }, cw: { type: "string", nullable: true, maxLength: 250 }, localOnly: { type: "boolean", default: false }, noExtractMentions: { type: "boolean", default: false }, @@ -398,6 +401,16 @@ export default define(meta, paramDef, async (ps, user) => { ps.text = null; } + if (ps.lang) { + if (!Object.keys(langmap).includes(ps.lang.trim())) + throw new Error("invalid param"); + ps.lang = ps.lang.trim().split("-")[0].split("@")[0]; + } else if (ps.text) { + ps.lang = detectLanguage(ps.text); + } else { + ps.lang = null; + } + let tags = []; let emojis = []; let mentionedUsers = []; @@ -585,6 +598,9 @@ export default define(meta, paramDef, async (ps, user) => { if (ps.text !== note.text) { update.text = ps.text; } + if (ps.lang !== note.lang) { + update.lang = ps.lang; + } if (ps.cw !== note.cw || (ps.cw && !note.cw)) { update.cw = ps.cw; } diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts index 22f9bb16e7..9551ae7c3f 100644 --- a/packages/backend/src/server/web/index.ts +++ b/packages/backend/src/server/web/index.ts @@ -512,7 +512,7 @@ router.get("/notes/:note", async (ctx, next) => { ctx.set("Cache-Control", "public, max-age=15"); ctx.set( "Content-Security-Policy", - "default-src 'self' 'unsafe-inline'; img-src *; frame-ancestors *", + "default-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src *; font-src 'self' data:; img-src *; media-src *; worker-src 'self'; frame-ancestors *", ); return; diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index ed0a86134e..0afcc07cf9 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -81,6 +81,8 @@ import { ScyllaPoll, } from "@/db/scylla.js"; import { userByIdCache, userDenormalizedCache } from "../user-cache.js"; +import { detect as detectLanguage } from "tinyld"; +import { langmap } from "@/misc/langmap.js"; export const mutedWordsCache = new Cache< { userId: UserProfile["userId"]; mutedWords: UserProfile["mutedWords"] }[] @@ -153,6 +155,7 @@ type Option = { createdAt?: Date | null; name?: string | null; text?: string | null; + lang?: string | null; reply?: Note | null; renote?: Note | null; files?: DriveFile[] | null; @@ -290,6 +293,16 @@ export default async ( data.text = null; } + if (data.lang) { + if (!Object.keys(langmap).includes(data.lang.trim())) + throw new Error("invalid param"); + data.lang = data.lang.trim().split("-")[0].split("@")[0]; + } else if (data.text) { + data.lang = detectLanguage(data.text); + } else { + data.lang = null; + } + let tags = data.apHashtags; let emojis = data.apEmojis; let mentionedUsers = data.apMentions; @@ -772,6 +785,7 @@ async function insertNote( : null, name: data.name, text: data.text, + lang: data.lang, hasPoll: data.poll != null, cw: data.cw == null ? null : data.cw, tags: tags.map((tag) => normalizeForSearch(tag)), diff --git a/packages/client/package.json b/packages/client/package.json index 6bd13bac95..82284cfb0b 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -61,6 +61,7 @@ "insert-text-at-cursor": "0.3.0", "json5": "2.2.3", "katex": "0.16.8", + "libopenmpt-wasm": "github:TheEssem/libopenmpt-packaging#build", "matter-js": "0.19.0", "mfm-js": "0.23.3", "photoswipe": "5.3.9", diff --git a/packages/client/src/components/MkInstanceTicker.vue b/packages/client/src/components/MkInstanceTicker.vue index f5bedee58a..6bd87201b9 100644 --- a/packages/client/src/components/MkInstanceTicker.vue +++ b/packages/client/src/components/MkInstanceTicker.vue @@ -4,7 +4,6 @@ v-tooltip="capitalize(instance.softwareName)" class="hpaizdrt" :style="bg" - @click.stop="openServerInfo" > {{ instance.name }} @@ -17,8 +16,6 @@ import { ref } from "vue"; import { instanceName } from "@/config"; import { instance as Instance } from "@/instance"; import { getProxiedImageUrlNullable } from "@/scripts/media-proxy"; -import { defaultStore } from "@/store"; -import { pageWindow } from "@/os"; const props = defineProps<{ instance?: { @@ -27,7 +24,6 @@ const props = defineProps<{ themeColor?: string; softwareName?: string; }; - host: string | null; }>(); const ticker = ref(null); @@ -87,13 +83,6 @@ function getInstanceIcon(instance): string { "/client-assets/dummy.png" ); } - -function openServerInfo() { - if (!defaultStore.state.openServerInfo) return; - const instanceInfoUrl = - props.host == null ? "/about" : `/instance-info/${props.host}`; - pageWindow(instanceInfoUrl); -} diff --git a/packages/client/src/components/MkNote.vue b/packages/client/src/components/MkNote.vue index 9b7dc9fdba..3581cd2395 100644 --- a/packages/client/src/components/MkNote.vue +++ b/packages/client/src/components/MkNote.vue @@ -97,7 +97,11 @@
- +
@@ -57,10 +57,12 @@ import MkInstanceTicker from "@/components/MkInstanceTicker.vue"; import { notePage } from "@/filters/note"; import { userPage } from "@/filters/user"; import { i18n } from "@/i18n"; +import { pageWindow } from "@/os"; const props = defineProps<{ note: misskey.entities.Note; pinned?: boolean; + canOpenServerInfo?: boolean; }>(); const note = ref(props.note); @@ -69,6 +71,15 @@ const showTicker = defaultStore.state.instanceTicker === "always" || (defaultStore.state.instanceTicker === "remote" && note.value.user.instance); + +function openServerInfo() { + if (!props.canOpenServerInfo || !defaultStore.state.openServerInfo) return; + const instanceInfoUrl = + note.value.user.host == null + ? "/about" + : `/instance-info/${note.value.user.host}`; + pageWindow(instanceInfoUrl); +}