diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b901f047..7f2b8ac44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,13 +20,14 @@ ### Changes - API: users/notes, notes/local-timeline で fileType 指定はできなくなりました -- API: notes/global-timeline は現在常に `[]` を返します - API: notes/featured でページネーションは他APIと同様 untilId を使って行うようになりました ### General - Feat: ユーザーごとに他ユーザーへの返信をタイムラインに含めるか設定可能になりました - Feat: ユーザーリスト内のメンバーごとに他ユーザーへの返信をユーザーリストタイムラインに含めるか設定可能になりました - Feat: ユーザーごとのハイライト +- Feat: プライバシーポリシー・運営者情報(Impressum)の指定が可能になりました + - プライバシーポリシーはサーバー登録時に同意確認が入ります - Enhance: ソフトワードミュートとハードワードミュートは統合されました - Enhance: モデレーションログ機能の強化 - Enhance: ローカリゼーションの更新 diff --git a/locales/index.d.ts b/locales/index.d.ts index 12ac0197f..d90f8fa6f 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1132,6 +1132,12 @@ export interface Locale { "showRepliesToOthersInTimeline": string; "hideRepliesToOthersInTimeline": string; "externalServices": string; + "impressum": string; + "impressumUrl": string; + "impressumDescription": string; + "privacyPolicy": string; + "privacyPolicyUrl": string; + "tosAndPrivacyPolicy": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index f2190022c..c92d83436 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1129,6 +1129,12 @@ fileAttachedOnly: "ファイル付きのみ" showRepliesToOthersInTimeline: "TLに他の人への返信を含める" hideRepliesToOthersInTimeline: "TLに他の人への返信を含めない" externalServices: "外部サービス" +impressum: "運営者情報" +impressumUrl: "運営者情報URL" +impressumDescription: "ドイツなどの一部の国と地域では表示が義務付けられています(Impressum)。" +privacyPolicy: "プライバシーポリシー" +privacyPolicyUrl: "プライバシーポリシーURL" +tosAndPrivacyPolicy: "利用規約・プライバシーポリシー" _announcement: forExistingUsers: "既存ユーザーのみ" diff --git a/package.json b/package.json index a52c773dd..df51a05f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2023.10.0-beta.5", + "version": "2023.10.0-beta.6", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/migration/1696003580220-AddSomeUrls.js b/packages/backend/migration/1696003580220-AddSomeUrls.js new file mode 100644 index 000000000..683aa5eee --- /dev/null +++ b/packages/backend/migration/1696003580220-AddSomeUrls.js @@ -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"`); + } +} diff --git a/packages/backend/src/core/HashtagService.ts b/packages/backend/src/core/HashtagService.ts index 4900fa90a..1a06767da 100644 --- a/packages/backend/src/core/HashtagService.ts +++ b/packages/backend/src/core/HashtagService.ts @@ -5,7 +5,6 @@ import { Inject, Injectable } from '@nestjs/common'; import * as Redis from 'ioredis'; -import { getLineAndCharacterOfPosition } from 'typescript'; import { DI } from '@/di-symbols.js'; import type { MiUser } from '@/models/User.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; @@ -15,6 +14,7 @@ import type { HashtagsRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { FeaturedService } from '@/core/FeaturedService.js'; +import { MetaService } from '@/core/MetaService.js'; @Injectable() export class HashtagService { @@ -28,6 +28,7 @@ export class HashtagService { private userEntityService: UserEntityService, private featuredService: FeaturedService, private idService: IdService, + private metaService: MetaService, ) { } @@ -157,7 +158,9 @@ export class HashtagService { @bindThis public async updateHashtagsRanking(hashtag: string, userId: MiUser['id']): Promise { - // TODO: instance.hiddenTagsの考慮 + const instance = await this.metaService.fetch(); + const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t)); + if (hiddenTags.includes(hashtag)) return; // YYYYMMDDHHmm (10分間隔) const now = new Date(); diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 491d44672..60fdaab54 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -335,6 +335,18 @@ export class MiMeta { }) 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', { length: 8192, nullable: true, diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 79f130dab..dd2b7882a 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -102,6 +102,8 @@ export class NodeinfoServerService { }, langs: meta.langs, tosUrl: meta.termsOfServiceUrl, + privacyPolicyUrl: meta.privacyPolicyUrl, + impressumUrl: meta.impressumUrl, repositoryUrl: meta.repositoryUrl, feedbackUrl: meta.feedbackUrl, disableRegistration: meta.disableRegistration, diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 53e367278..ddca5b470 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -331,6 +331,8 @@ export default class extends Endpoint { // eslint- tosUrl: instance.termsOfServiceUrl, repositoryUrl: instance.repositoryUrl, feedbackUrl: instance.feedbackUrl, + impressumUrl: instance.impressumUrl, + privacyPolicyUrl: instance.privacyPolicyUrl, disableRegistration: instance.disableRegistration, emailRequiredForSignup: instance.emailRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, 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 247d3ba4e..ee7c564e2 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -86,6 +86,8 @@ export const paramDef = { tosUrl: { type: 'string', nullable: true }, repositoryUrl: { type: 'string' }, feedbackUrl: { type: 'string' }, + impressumUrl: { type: 'string' }, + privacyPolicyUrl: { type: 'string' }, useObjectStorage: { type: 'boolean' }, objectStorageBaseUrl: { type: 'string', nullable: true }, objectStorageBucket: { type: 'string', nullable: true }, @@ -345,6 +347,14 @@ export default class extends Endpoint { // eslint- set.feedbackUrl = ps.feedbackUrl; } + if (ps.impressumUrl !== undefined) { + set.impressumUrl = ps.impressumUrl; + } + + if (ps.privacyPolicyUrl !== undefined) { + set.privacyPolicyUrl = ps.privacyPolicyUrl; + } + if (ps.useObjectStorage !== undefined) { set.useObjectStorage = ps.useObjectStorage; } diff --git a/packages/backend/src/server/api/endpoints/hashtags/trend.ts b/packages/backend/src/server/api/endpoints/hashtags/trend.ts index a69e007a4..8f382eb96 100644 --- a/packages/backend/src/server/api/endpoints/hashtags/trend.ts +++ b/packages/backend/src/server/api/endpoints/hashtags/trend.ts @@ -5,8 +5,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import { MetaService } from '@/core/MetaService.js'; import { DI } from '@/di-symbols.js'; import { FeaturedService } from '@/core/FeaturedService.js'; import { HashtagService } from '@/core/HashtagService.js'; @@ -55,14 +53,10 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( - private metaService: MetaService, private featuredService: FeaturedService, private hashtagService: HashtagService, ) { super(meta, paramDef, async () => { - const instance = await this.metaService.fetch(true); - const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t)); - const ranking = await this.featuredService.getHashtagsRanking(10); const charts = ranking.length === 0 ? {} : await this.hashtagService.getCharts(ranking, 20); diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 271b3f6fb..e7c8d2682 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -299,6 +299,8 @@ export default class extends Endpoint { // eslint- tosUrl: instance.termsOfServiceUrl, repositoryUrl: instance.repositoryUrl, feedbackUrl: instance.feedbackUrl, + impressumUrl: instance.impressumUrl, + privacyPolicyUrl: instance.privacyPolicyUrl, disableRegistration: instance.disableRegistration, emailRequiredForSignup: instance.emailRequiredForSignup, enableHcaptcha: instance.enableHcaptcha, diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts index e5a86905d..be7557c21 100644 --- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts @@ -67,8 +67,37 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.gtlDisabled); } - // TODO? - return []; + //#region Construct query + const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), + ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) + .andWhere('note.visibility = \'public\'') + .andWhere('note.channelId IS NULL') + .innerJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.reply', 'reply') + .leftJoinAndSelect('note.renote', 'renote') + .leftJoinAndSelect('reply.user', 'replyUser') + .leftJoinAndSelect('renote.user', 'renoteUser'); + + if (me) { + this.queryService.generateMutedUserQuery(query, me); + this.queryService.generateBlockedUserQuery(query, me); + this.queryService.generateMutedUserRenotesQueryForNotes(query, me); + } + + if (ps.withFiles) { + query.andWhere('note.fileIds != \'{}\''); + } + //#endregion + + const timeline = await query.limit(ps.limit).getMany(); + + process.nextTick(() => { + if (me) { + this.activeUsersChart.read(me); + } + }); + + return await this.noteEntityService.packMany(timeline, me); }); } } diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue index aa4a184d7..76163ab68 100644 --- a/packages/frontend/src/components/MkSignupDialog.rules.vue +++ b/packages/frontend/src/components/MkSignupDialog.rules.vue @@ -30,13 +30,15 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.agree }} - - - + + + + - {{ i18n.ts.termsOfService }} - - {{ i18n.ts.agree }} + {{ i18n.ts.agree }} @@ -70,14 +72,15 @@ import MkInfo from '@/components/MkInfo.vue'; import * as os from '@/os.js'; 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 agreeTos = ref(false); +const agreeTosAndPrivacyPolicy = ref(false); const agreeNote = ref(false); 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<{ @@ -85,6 +88,18 @@ const emit = defineEmits<{ (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) { if (v) { 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) { const confirm = await os.confirm({ type: 'question', title: i18n.ts.doYouAgree, - text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.termsOfService }), + text: i18n.t('iHaveReadXCarefullyAndAgree', { + x: tosPrivacyPolicyLabel.value, + }), }); if (confirm.canceled) return; - agreeTos.value = true; + agreeTosAndPrivacyPolicy.value = true; } else { - agreeTos.value = false; + agreeTosAndPrivacyPolicy.value = false; } } diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue index e4520bbb2..40493a5d0 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.vue @@ -104,7 +104,25 @@ function showMenu(ev) { action: () => { 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, icon: 'ti ti-help-circle', action: () => { diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue index 02768b077..ee4043f9a 100644 --- a/packages/frontend/src/pages/about.vue +++ b/packages/frontend/src/pages/about.vue @@ -46,14 +46,18 @@ SPDX-License-Identifier: AGPL-3.0-only - - + {{ i18n.ts.impressum }} + diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index 46f92729e..8b160635f 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -25,6 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + @@ -69,6 +74,7 @@ let emailRequiredForSignup: boolean = $ref(false); let sensitiveWords: string = $ref(''); let preservedUsernames: string = $ref(''); let tosUrl: string | null = $ref(null); +let privacyPolicyUrl: string | null = $ref(null); async function init() { const meta = await os.api('admin/meta'); @@ -77,6 +83,7 @@ async function init() { sensitiveWords = meta.sensitiveWords.join('\n'); preservedUsernames = meta.preservedUsernames.join('\n'); tosUrl = meta.tosUrl; + privacyPolicyUrl = meta.privacyPolicyUrl; } function save() { @@ -84,6 +91,7 @@ function save() { disableRegistration: !enableRegistration, emailRequiredForSignup, tosUrl, + privacyPolicyUrl, sensitiveWords: sensitiveWords.split('\n'), preservedUsernames: preservedUsernames.split('\n'), }).then(() => { diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index 09a6cc7e2..2e3f1611d 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -34,6 +34,12 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + + @@ -135,6 +141,7 @@ let shortName: string | null = $ref(null); let description: string | null = $ref(null); let maintainerName: string | null = $ref(null); let maintainerEmail: string | null = $ref(null); +let impressumUrl: string | null = $ref(null); let pinnedUsers: string = $ref(''); let cacheRemoteFiles: boolean = $ref(false); let cacheRemoteSensitiveFiles: boolean = $ref(false); @@ -153,6 +160,7 @@ async function init(): Promise { description = meta.description; maintainerName = meta.maintainerName; maintainerEmail = meta.maintainerEmail; + impressumUrl = meta.impressumUrl; pinnedUsers = meta.pinnedUsers.join('\n'); cacheRemoteFiles = meta.cacheRemoteFiles; cacheRemoteSensitiveFiles = meta.cacheRemoteSensitiveFiles; @@ -172,6 +180,7 @@ function save(): void { description, maintainerName, maintainerEmail, + impressumUrl, pinnedUsers: pinnedUsers.split('\n'), cacheRemoteFiles, cacheRemoteSensitiveFiles, diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index ca4a71a67..e075e05db 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -68,7 +68,25 @@ export function openInstanceMenu(ev: MouseEvent) { text: i18n.ts.manageCustomEmojis, icon: 'ti ti-icons', } : 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, icon: 'ti ti-help-circle', action: () => { diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index fa09fd94e..fe913886c 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -2408,6 +2408,8 @@ type LiteInstanceMetadata = { tosUrl: string | null; repositoryUrl: string; feedbackUrl: string; + impressumUrl: string | null; + privacyPolicyUrl: string | null; disableRegistration: boolean; disableLocalTimeline: 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: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: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 // (No @packageDocumentation comment for this package) diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 8097618c3..b60056e21 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -322,6 +322,8 @@ export type LiteInstanceMetadata = { tosUrl: string | null; repositoryUrl: string; feedbackUrl: string; + impressumUrl: string | null; + privacyPolicyUrl: string | null; disableRegistration: boolean; disableLocalTimeline: boolean; disableGlobalTimeline: boolean;