プライバシーポリシー・運営者情報のリンクを追加 (#11925)

* 運営者情報・プライバシーポリシーリンクを追加

* Update Changelog

* Run api extractor

* プライバシーポリシー・利用規約の同意をまとめる

* Update Changelog

* fix lint

* fix

* api extractor

* improve design

* nodeinfoにプライバシーポリシー・運営者情報を追加
This commit is contained in:
かっこかり 2023-10-07 13:13:13 +09:00 committed by GitHub
parent d6ef28d4ca
commit 5e8c0deab3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 160 additions and 23 deletions

View File

@ -27,6 +27,8 @@
- Feat: ユーザーごとに他ユーザーへの返信をタイムラインに含めるか設定可能になりました - Feat: ユーザーごとに他ユーザーへの返信をタイムラインに含めるか設定可能になりました
- Feat: ユーザーリスト内のメンバーごとに他ユーザーへの返信をユーザーリストタイムラインに含めるか設定可能になりました - Feat: ユーザーリスト内のメンバーごとに他ユーザーへの返信をユーザーリストタイムラインに含めるか設定可能になりました
- Feat: ユーザーごとのハイライト - Feat: ユーザーごとのハイライト
- Feat: プライバシーポリシー・運営者情報Impressumの指定が可能になりました
- プライバシーポリシーはサーバー登録時に同意確認が入ります
- Enhance: ソフトワードミュートとハードワードミュートは統合されました - Enhance: ソフトワードミュートとハードワードミュートは統合されました
- Enhance: モデレーションログ機能の強化 - Enhance: モデレーションログ機能の強化
- Enhance: ローカリゼーションの更新 - Enhance: ローカリゼーションの更新

6
locales/index.d.ts vendored
View File

@ -1132,6 +1132,12 @@ export interface Locale {
"showRepliesToOthersInTimeline": string; "showRepliesToOthersInTimeline": string;
"hideRepliesToOthersInTimeline": string; "hideRepliesToOthersInTimeline": string;
"externalServices": string; "externalServices": string;
"impressum": string;
"impressumUrl": string;
"impressumDescription": string;
"privacyPolicy": string;
"privacyPolicyUrl": string;
"tosAndPrivacyPolicy": string;
"_announcement": { "_announcement": {
"forExistingUsers": string; "forExistingUsers": string;
"forExistingUsersDescription": string; "forExistingUsersDescription": string;

View File

@ -1129,6 +1129,12 @@ fileAttachedOnly: "ファイル付きのみ"
showRepliesToOthersInTimeline: "TLに他の人への返信を含める" showRepliesToOthersInTimeline: "TLに他の人への返信を含める"
hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない" hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない"
externalServices: "外部サービス" externalServices: "外部サービス"
impressum: "運営者情報"
impressumUrl: "運営者情報URL"
impressumDescription: "ドイツなどの一部の国と地域では表示が義務付けられています(Impressum)。"
privacyPolicy: "プライバシーポリシー"
privacyPolicyUrl: "プライバシーポリシーURL"
tosAndPrivacyPolicy: "利用規約・プライバシーポリシー"
_announcement: _announcement:
forExistingUsers: "既存ユーザーのみ" forExistingUsers: "既存ユーザーのみ"

View File

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class AddSomeUrls1696003580220 {
name = 'AddSomeUrls1696003580220'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "impressumUrl" character varying(1024)`);
await queryRunner.query(`ALTER TABLE "meta" ADD "privacyPolicyUrl" character varying(1024)`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "impressumUrl"`);
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "privacyPolicyUrl"`);
}
}

View File

@ -335,6 +335,18 @@ export class MiMeta {
}) })
public feedbackUrl: string | null; public feedbackUrl: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public impressumUrl: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public privacyPolicyUrl: string | null;
@Column('varchar', { @Column('varchar', {
length: 8192, length: 8192,
nullable: true, nullable: true,

View File

@ -102,6 +102,8 @@ export class NodeinfoServerService {
}, },
langs: meta.langs, langs: meta.langs,
tosUrl: meta.termsOfServiceUrl, tosUrl: meta.termsOfServiceUrl,
privacyPolicyUrl: meta.privacyPolicyUrl,
impressumUrl: meta.impressumUrl,
repositoryUrl: meta.repositoryUrl, repositoryUrl: meta.repositoryUrl,
feedbackUrl: meta.feedbackUrl, feedbackUrl: meta.feedbackUrl,
disableRegistration: meta.disableRegistration, disableRegistration: meta.disableRegistration,

View File

@ -331,6 +331,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
tosUrl: instance.termsOfServiceUrl, tosUrl: instance.termsOfServiceUrl,
repositoryUrl: instance.repositoryUrl, repositoryUrl: instance.repositoryUrl,
feedbackUrl: instance.feedbackUrl, feedbackUrl: instance.feedbackUrl,
impressumUrl: instance.impressumUrl,
privacyPolicyUrl: instance.privacyPolicyUrl,
disableRegistration: instance.disableRegistration, disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup, emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha, enableHcaptcha: instance.enableHcaptcha,

View File

@ -86,6 +86,8 @@ export const paramDef = {
tosUrl: { type: 'string', nullable: true }, tosUrl: { type: 'string', nullable: true },
repositoryUrl: { type: 'string' }, repositoryUrl: { type: 'string' },
feedbackUrl: { type: 'string' }, feedbackUrl: { type: 'string' },
impressumUrl: { type: 'string' },
privacyPolicyUrl: { type: 'string' },
useObjectStorage: { type: 'boolean' }, useObjectStorage: { type: 'boolean' },
objectStorageBaseUrl: { type: 'string', nullable: true }, objectStorageBaseUrl: { type: 'string', nullable: true },
objectStorageBucket: { type: 'string', nullable: true }, objectStorageBucket: { type: 'string', nullable: true },
@ -345,6 +347,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.feedbackUrl = ps.feedbackUrl; set.feedbackUrl = ps.feedbackUrl;
} }
if (ps.impressumUrl !== undefined) {
set.impressumUrl = ps.impressumUrl;
}
if (ps.privacyPolicyUrl !== undefined) {
set.privacyPolicyUrl = ps.privacyPolicyUrl;
}
if (ps.useObjectStorage !== undefined) { if (ps.useObjectStorage !== undefined) {
set.useObjectStorage = ps.useObjectStorage; set.useObjectStorage = ps.useObjectStorage;
} }

View File

@ -299,6 +299,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
tosUrl: instance.termsOfServiceUrl, tosUrl: instance.termsOfServiceUrl,
repositoryUrl: instance.repositoryUrl, repositoryUrl: instance.repositoryUrl,
feedbackUrl: instance.feedbackUrl, feedbackUrl: instance.feedbackUrl,
impressumUrl: instance.impressumUrl,
privacyPolicyUrl: instance.privacyPolicyUrl,
disableRegistration: instance.disableRegistration, disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup, emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha, enableHcaptcha: instance.enableHcaptcha,

View File

@ -30,13 +30,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch :modelValue="agreeServerRules" style="margin-top: 16px;" @update:modelValue="updateAgreeServerRules">{{ i18n.ts.agree }}</MkSwitch> <MkSwitch :modelValue="agreeServerRules" style="margin-top: 16px;" @update:modelValue="updateAgreeServerRules">{{ i18n.ts.agree }}</MkSwitch>
</MkFolder> </MkFolder>
<MkFolder v-if="availableTos" :defaultOpen="true"> <MkFolder v-if="availableTos || availablePrivacyPolicy" :defaultOpen="true">
<template #label>{{ i18n.ts.termsOfService }}</template> <template #label>{{ tosPrivacyPolicyLabel }}</template>
<template #suffix><i v-if="agreeTos" class="ti ti-check" style="color: var(--success)"></i></template> <template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--success)"></i></template>
<div class="_gaps_s">
<div v-if="availableTos"><a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a></div>
<div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl" class="_link" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ti ti-external-link"></i></a></div>
</div>
<a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a> <MkSwitch :modelValue="agreeTosAndPrivacyPolicy" style="margin-top: 16px;" @update:modelValue="updateAgreeTosAndPrivacyPolicy">{{ i18n.ts.agree }}</MkSwitch>
<MkSwitch :modelValue="agreeTos" style="margin-top: 16px;" @update:modelValue="updateAgreeTos">{{ i18n.ts.agree }}</MkSwitch>
</MkFolder> </MkFolder>
<MkFolder :defaultOpen="true"> <MkFolder :defaultOpen="true">
@ -70,14 +72,15 @@ import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
const availableServerRules = instance.serverRules.length > 0; const availableServerRules = instance.serverRules.length > 0;
const availableTos = instance.tosUrl != null; const availableTos = instance.tosUrl != null && instance.tosUrl !== '';
const availablePrivacyPolicy = instance.privacyPolicyUrl != null && instance.privacyPolicyUrl !== '';
const agreeServerRules = ref(false); const agreeServerRules = ref(false);
const agreeTos = ref(false); const agreeTosAndPrivacyPolicy = ref(false);
const agreeNote = ref(false); const agreeNote = ref(false);
const agreed = computed(() => { const agreed = computed(() => {
return (!availableServerRules || agreeServerRules.value) && (!availableTos || agreeTos.value) && agreeNote.value; return (!availableServerRules || agreeServerRules.value) && ((!availableTos && !availablePrivacyPolicy) || agreeTosAndPrivacyPolicy.value) && agreeNote.value;
}); });
const emit = defineEmits<{ const emit = defineEmits<{
@ -85,6 +88,18 @@ const emit = defineEmits<{
(ev: 'done'): void; (ev: 'done'): void;
}>(); }>();
const tosPrivacyPolicyLabel = computed(() => {
if (availableTos && availablePrivacyPolicy) {
return i18n.ts.tosAndPrivacyPolicy;
} else if (availableTos) {
return i18n.ts.termsOfService;
} else if (availablePrivacyPolicy) {
return i18n.ts.privacyPolicy;
} else {
return "";
}
});
async function updateAgreeServerRules(v: boolean) { async function updateAgreeServerRules(v: boolean) {
if (v) { if (v) {
const confirm = await os.confirm({ const confirm = await os.confirm({
@ -99,17 +114,19 @@ async function updateAgreeServerRules(v: boolean) {
} }
} }
async function updateAgreeTos(v: boolean) { async function updateAgreeTosAndPrivacyPolicy(v: boolean) {
if (v) { if (v) {
const confirm = await os.confirm({ const confirm = await os.confirm({
type: 'question', type: 'question',
title: i18n.ts.doYouAgree, title: i18n.ts.doYouAgree,
text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.termsOfService }), text: i18n.t('iHaveReadXCarefullyAndAgree', {
x: tosPrivacyPolicyLabel.value,
}),
}); });
if (confirm.canceled) return; if (confirm.canceled) return;
agreeTos.value = true; agreeTosAndPrivacyPolicy.value = true;
} else { } else {
agreeTos.value = false; agreeTosAndPrivacyPolicy.value = false;
} }
} }

View File

@ -104,7 +104,25 @@ function showMenu(ev) {
action: () => { action: () => {
os.pageWindow('/about-misskey'); os.pageWindow('/about-misskey');
}, },
}, null, { }, null, (instance.impressumUrl) ? {
text: i18n.ts.impressum,
icon: 'ti ti-file-invoice',
action: () => {
window.open(instance.impressumUrl, '_blank');
},
} : undefined, (instance.tosUrl) ? {
text: i18n.ts.termsOfService,
icon: 'ti ti-notebook',
action: () => {
window.open(instance.tosUrl, '_blank');
},
} : undefined, (instance.privacyPolicyUrl) ? {
text: i18n.ts.privacyPolicy,
icon: 'ti ti-shield-lock',
action: () => {
window.open(instance.privacyPolicyUrl, '_blank');
},
} : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : null, {
text: i18n.ts.help, text: i18n.ts.help,
icon: 'ti ti-help-circle', icon: 'ti ti-help-circle',
action: () => { action: () => {

View File

@ -46,14 +46,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #value>{{ instance.maintainerEmail }}</template> <template #value>{{ instance.maintainerEmail }}</template>
</MkKeyValue> </MkKeyValue>
</FormSplit> </FormSplit>
<FormLink v-if="instance.impressumUrl" :to="instance.impressumUrl" external>{{ i18n.ts.impressum }}</FormLink>
<div class="_formLinks">
<MkFolder v-if="instance.serverRules.length > 0"> <MkFolder v-if="instance.serverRules.length > 0">
<template #label>{{ i18n.ts.serverRules }}</template> <template #label>{{ i18n.ts.serverRules }}</template>
<ol class="_gaps_s" :class="$style.rules"> <ol class="_gaps_s" :class="$style.rules">
<li v-for="item in instance.serverRules" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li> <li v-for="item, index in instance.serverRules" :key="index" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
</ol> </ol>
</MkFolder> </MkFolder>
<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>{{ i18n.ts.termsOfService }}</FormLink> <FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>{{ i18n.ts.termsOfService }}</FormLink>
<FormLink v-if="instance.privacyPolicyUrl" :to="instance.privacyPolicyUrl" external>{{ i18n.ts.privacyPolicy }}</FormLink>
</div>
</div> </div>
</FormSection> </FormSection>

View File

@ -25,6 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.tosUrl }}</template> <template #label>{{ i18n.ts.tosUrl }}</template>
</MkInput> </MkInput>
<MkInput v-model="privacyPolicyUrl">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts.privacyPolicyUrl }}</template>
</MkInput>
<MkTextarea v-model="preservedUsernames"> <MkTextarea v-model="preservedUsernames">
<template #label>{{ i18n.ts.preservedUsernames }}</template> <template #label>{{ i18n.ts.preservedUsernames }}</template>
<template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template> <template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
@ -69,6 +74,7 @@ let emailRequiredForSignup: boolean = $ref(false);
let sensitiveWords: string = $ref(''); let sensitiveWords: string = $ref('');
let preservedUsernames: string = $ref(''); let preservedUsernames: string = $ref('');
let tosUrl: string | null = $ref(null); let tosUrl: string | null = $ref(null);
let privacyPolicyUrl: string | null = $ref(null);
async function init() { async function init() {
const meta = await os.api('admin/meta'); const meta = await os.api('admin/meta');
@ -77,6 +83,7 @@ async function init() {
sensitiveWords = meta.sensitiveWords.join('\n'); sensitiveWords = meta.sensitiveWords.join('\n');
preservedUsernames = meta.preservedUsernames.join('\n'); preservedUsernames = meta.preservedUsernames.join('\n');
tosUrl = meta.tosUrl; tosUrl = meta.tosUrl;
privacyPolicyUrl = meta.privacyPolicyUrl;
} }
function save() { function save() {
@ -84,6 +91,7 @@ function save() {
disableRegistration: !enableRegistration, disableRegistration: !enableRegistration,
emailRequiredForSignup, emailRequiredForSignup,
tosUrl, tosUrl,
privacyPolicyUrl,
sensitiveWords: sensitiveWords.split('\n'), sensitiveWords: sensitiveWords.split('\n'),
preservedUsernames: preservedUsernames.split('\n'), preservedUsernames: preservedUsernames.split('\n'),
}).then(() => { }).then(() => {

View File

@ -34,6 +34,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput> </MkInput>
</FormSplit> </FormSplit>
<MkInput v-model="impressumUrl">
<template #label>{{ i18n.ts.impressumUrl }}</template>
<template #prefix><i class="ti ti-link"></i></template>
<template #caption>{{ i18n.ts.impressumDescription }}</template>
</MkInput>
<MkTextarea v-model="pinnedUsers"> <MkTextarea v-model="pinnedUsers">
<template #label>{{ i18n.ts.pinnedUsers }}</template> <template #label>{{ i18n.ts.pinnedUsers }}</template>
<template #caption>{{ i18n.ts.pinnedUsersDescription }}</template> <template #caption>{{ i18n.ts.pinnedUsersDescription }}</template>
@ -135,6 +141,7 @@ let shortName: string | null = $ref(null);
let description: string | null = $ref(null); let description: string | null = $ref(null);
let maintainerName: string | null = $ref(null); let maintainerName: string | null = $ref(null);
let maintainerEmail: string | null = $ref(null); let maintainerEmail: string | null = $ref(null);
let impressumUrl: string | null = $ref(null);
let pinnedUsers: string = $ref(''); let pinnedUsers: string = $ref('');
let cacheRemoteFiles: boolean = $ref(false); let cacheRemoteFiles: boolean = $ref(false);
let cacheRemoteSensitiveFiles: boolean = $ref(false); let cacheRemoteSensitiveFiles: boolean = $ref(false);
@ -153,6 +160,7 @@ async function init(): Promise<void> {
description = meta.description; description = meta.description;
maintainerName = meta.maintainerName; maintainerName = meta.maintainerName;
maintainerEmail = meta.maintainerEmail; maintainerEmail = meta.maintainerEmail;
impressumUrl = meta.impressumUrl;
pinnedUsers = meta.pinnedUsers.join('\n'); pinnedUsers = meta.pinnedUsers.join('\n');
cacheRemoteFiles = meta.cacheRemoteFiles; cacheRemoteFiles = meta.cacheRemoteFiles;
cacheRemoteSensitiveFiles = meta.cacheRemoteSensitiveFiles; cacheRemoteSensitiveFiles = meta.cacheRemoteSensitiveFiles;
@ -172,6 +180,7 @@ function save(): void {
description, description,
maintainerName, maintainerName,
maintainerEmail, maintainerEmail,
impressumUrl,
pinnedUsers: pinnedUsers.split('\n'), pinnedUsers: pinnedUsers.split('\n'),
cacheRemoteFiles, cacheRemoteFiles,
cacheRemoteSensitiveFiles, cacheRemoteSensitiveFiles,

View File

@ -68,7 +68,25 @@ export function openInstanceMenu(ev: MouseEvent) {
text: i18n.ts.manageCustomEmojis, text: i18n.ts.manageCustomEmojis,
icon: 'ti ti-icons', icon: 'ti ti-icons',
} : undefined], } : undefined],
}, null, { }, null, (instance.impressumUrl) ? {
text: i18n.ts.impressum,
icon: 'ti ti-file-invoice',
action: () => {
window.open(instance.impressumUrl, '_blank');
},
} : undefined, (instance.tosUrl) ? {
text: i18n.ts.termsOfService,
icon: 'ti ti-notebook',
action: () => {
window.open(instance.tosUrl, '_blank');
},
} : undefined, (instance.privacyPolicyUrl) ? {
text: i18n.ts.privacyPolicy,
icon: 'ti ti-shield-lock',
action: () => {
window.open(instance.privacyPolicyUrl, '_blank');
},
} : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : null, {
text: i18n.ts.help, text: i18n.ts.help,
icon: 'ti ti-help-circle', icon: 'ti ti-help-circle',
action: () => { action: () => {

View File

@ -2408,6 +2408,8 @@ type LiteInstanceMetadata = {
tosUrl: string | null; tosUrl: string | null;
repositoryUrl: string; repositoryUrl: string;
feedbackUrl: string; feedbackUrl: string;
impressumUrl: string | null;
privacyPolicyUrl: string | null;
disableRegistration: boolean; disableRegistration: boolean;
disableLocalTimeline: boolean; disableLocalTimeline: boolean;
disableGlobalTimeline: boolean; disableGlobalTimeline: boolean;
@ -2978,7 +2980,7 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts // src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
// src/api.types.ts:630:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts // src/api.types.ts:630:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
// src/entities.ts:107:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts // src/entities.ts:107:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
// src/entities.ts:594:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/entities.ts:596:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package) // (No @packageDocumentation comment for this package)

View File

@ -322,6 +322,8 @@ export type LiteInstanceMetadata = {
tosUrl: string | null; tosUrl: string | null;
repositoryUrl: string; repositoryUrl: string;
feedbackUrl: string; feedbackUrl: string;
impressumUrl: string | null;
privacyPolicyUrl: string | null;
disableRegistration: boolean; disableRegistration: boolean;
disableLocalTimeline: boolean; disableLocalTimeline: boolean;
disableGlobalTimeline: boolean; disableGlobalTimeline: boolean;