From c6e3ec07d1e2a66eb8f0660ebd699a103f690345 Mon Sep 17 00:00:00 2001
From: Marie <marie@kaifa.ch>
Date: Fri, 26 Jan 2024 21:29:38 +0100
Subject: [PATCH] add: DeepLX-JS support

Closes #324
---
 .../backend/migration/1706232992000-deeplx.js | 18 +++++++
 packages/backend/src/models/Meta.ts           | 11 +++++
 .../src/server/api/endpoints/admin/meta.ts    | 10 ++++
 .../server/api/endpoints/admin/update-meta.ts | 14 ++++++
 .../backend/src/server/api/endpoints/meta.ts  |  2 +-
 .../server/api/endpoints/notes/translate.ts   | 49 ++++++++++++++-----
 .../src/pages/admin/external-services.vue     | 13 +++++
 7 files changed, 103 insertions(+), 14 deletions(-)
 create mode 100644 packages/backend/migration/1706232992000-deeplx.js

diff --git a/packages/backend/migration/1706232992000-deeplx.js b/packages/backend/migration/1706232992000-deeplx.js
new file mode 100644
index 0000000000..5c763dbf8b
--- /dev/null
+++ b/packages/backend/migration/1706232992000-deeplx.js
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class Deeplx1706232992000 {
+    name = 'Deeplx1706232992000';
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "deeplFreeMode" boolean NOT NULL DEFAULT false`);
+		await queryRunner.query(`ALTER TABLE "meta" ADD "deeplFreeInstance" character varying(1024)`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "deeplFreeMode"`);
+		await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "deeplFreeInstance"`);
+    }
+}
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index 6d5c4b3746..9629012c74 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -353,6 +353,17 @@ export class MiMeta {
 	})
 	public deeplIsPro: boolean;
 
+	@Column('boolean', {
+		default: false,
+	})
+	public deeplFreeMode: boolean;
+
+	@Column('varchar', {
+		length: 1024,
+		nullable: true,
+	})
+	public deeplFreeInstance: string | null;
+
 	@Column('varchar', {
 		length: 1024,
 		nullable: true,
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 9fe997f889..c6edd6c9a1 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -395,6 +395,14 @@ export const meta = {
 				type: 'boolean',
 				optional: false, nullable: false,
 			},
+			deeplFreeMode: {
+				type: 'boolean',
+				optional: false, nullable: false,
+			},
+			deeplFreeInstance: {
+				type: 'string',
+				optional: false, nullable: true,
+			},
 			defaultDarkTheme: {
 				type: 'string',
 				optional: false, nullable: true,
@@ -576,6 +584,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				objectStorageS3ForcePathStyle: instance.objectStorageS3ForcePathStyle,
 				deeplAuthKey: instance.deeplAuthKey,
 				deeplIsPro: instance.deeplIsPro,
+				deeplFreeMode: instance.deeplFreeMode,
+				deeplFreeInstance: instance.deeplFreeInstance,
 				enableIpLogging: instance.enableIpLogging,
 				enableActiveEmailValidation: instance.enableActiveEmailValidation,
 				enableVerifymailApi: instance.enableVerifymailApi,
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 786a628d60..8c0d2f8876 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -91,6 +91,8 @@ export const paramDef = {
 		summalyProxy: { type: 'string', nullable: true },
 		deeplAuthKey: { type: 'string', nullable: true },
 		deeplIsPro: { type: 'boolean' },
+		deeplFreeMode: { type: 'boolean' },
+		deeplFreeInstance: { type: 'string', nullable: true },
 		enableEmail: { type: 'boolean' },
 		email: { type: 'string', nullable: true },
 		smtpSecure: { type: 'boolean' },
@@ -479,6 +481,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				set.deeplIsPro = ps.deeplIsPro;
 			}
 
+			if (ps.deeplFreeMode !== undefined) {
+				set.deeplFreeMode = ps.deeplFreeMode;
+			}
+
+			if (ps.deeplFreeInstance !== undefined) {
+				if (ps.deeplFreeInstance === '') {
+					set.deeplFreeInstance = null;
+				} else {
+					set.deeplFreeInstance = ps.deeplFreeInstance;
+				}
+			}
+
 			if (ps.enableIpLogging !== undefined) {
 				set.enableIpLogging = ps.enableIpLogging;
 			}
diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts
index af779aa850..8367536ad9 100644
--- a/packages/backend/src/server/api/endpoints/meta.ts
+++ b/packages/backend/src/server/api/endpoints/meta.ts
@@ -411,7 +411,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				enableEmail: instance.enableEmail,
 				enableServiceWorker: instance.enableServiceWorker,
 
-				translatorAvailable: instance.deeplAuthKey != null,
+				translatorAvailable: instance.deeplAuthKey != null || instance.deeplFreeMode && instance.deeplFreeInstance,
 
 				serverRules: instance.serverRules,
 
diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts
index 698c37b616..2afa515f9d 100644
--- a/packages/backend/src/server/api/endpoints/notes/translate.ts
+++ b/packages/backend/src/server/api/endpoints/notes/translate.ts
@@ -81,19 +81,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			const instance = await this.metaService.fetch();
 
-			if (instance.deeplAuthKey == null) {
+			if (instance.deeplAuthKey == null && !instance.deeplFreeMode) {
 				return 204; // TODO: 良い感じのエラー返す
 			}
 
+			if (instance.deeplFreeMode && !instance.deeplFreeInstance) {
+				return 204;
+			}
+
 			let targetLang = ps.targetLang;
 			if (targetLang.includes('-')) targetLang = targetLang.split('-')[0];
 
 			const params = new URLSearchParams();
-			params.append('auth_key', instance.deeplAuthKey);
+			if (instance.deeplAuthKey) params.append('auth_key', instance.deeplAuthKey);
 			params.append('text', note.text);
 			params.append('target_lang', targetLang);
 
-			const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
+			const endpoint = instance.deeplFreeMode && instance.deeplFreeInstance ? `https://${instance.deeplFreeInstance}` : instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
 
 			const res = await this.httpRequestService.send(endpoint, {
 				method: 'POST',
@@ -103,18 +107,37 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				},
 				body: params.toString(),
 			});
+			if (instance.deeplAuthKey) {
+				const json = (await res.json()) as {
+					translations: {
+						detected_source_language: string;
+						text: string;
+					}[];
+				};
 
-			const json = (await res.json()) as {
-				translations: {
-					detected_source_language: string;
-					text: string;
-				}[];
-			};
+				return {
+					sourceLang: json.translations[0].detected_source_language,
+					text: json.translations[0].text,
+				};
+			} else {
+				const json = (await res.json()) as {
+					code: number,
+					message: string,
+					data: string,
+					source_lang: string,
+					target_lang: string,
+					alternatives: string[],
+				};
 
-			return {
-				sourceLang: json.translations[0].detected_source_language,
-				text: json.translations[0].text,
-			};
+				const languageNames = new Intl.DisplayNames(['en'], {
+					type: 'language',
+				});
+
+				return {
+					sourceLang: languageNames.of(json.source_lang),
+					text: json.data,
+				};
+			}
 		});
 	}
 }
diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue
index 1a67a668de..9994b4641f 100644
--- a/packages/frontend/src/pages/admin/external-services.vue
+++ b/packages/frontend/src/pages/admin/external-services.vue
@@ -19,6 +19,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkSwitch v-model="deeplIsPro">
 						<template #label>Pro account</template>
 					</MkSwitch>
+					<MkSwitch v-model="deeplFreeMode">
+						<template #label>Use DeepLX-JS (No Auth Key)</template>
+					</MkSwitch>
+					<MkInput v-if="deeplFreeMode" v-model="deeplFreeInstance" :placeholder="'example.com/translate'">
+						<template #prefix>https://</template>
+						<template #label>DeepLX-JS URL</template>
+					</MkInput>
 				</div>
 			</FormSection>
 		</FormSuspense>
@@ -49,17 +56,23 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
 
 const deeplAuthKey = ref<string>('');
 const deeplIsPro = ref<boolean>(false);
+const deeplFreeMode = ref<boolean>(false);
+const deeplFreeInstance = ref<string>('');
 
 async function init() {
 	const meta = await misskeyApi('admin/meta');
 	deeplAuthKey.value = meta.deeplAuthKey;
 	deeplIsPro.value = meta.deeplIsPro;
+	deeplFreeMode.value = meta.deeplFreeMode;
+	deeplFreeInstance.value = meta.deeplFreeInstance;
 }
 
 function save() {
 	os.apiWithDialog('admin/update-meta', {
 		deeplAuthKey: deeplAuthKey.value,
 		deeplIsPro: deeplIsPro.value,
+		deeplFreeMode: deeplFreeMode.value,
+		deeplFreeInstance: deeplFreeInstance.value,
 	}).then(() => {
 		fetchInstance();
 	});