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);
+}