Skip to content

Commit

Permalink
Add GTL instance mutes
Browse files Browse the repository at this point in the history
  • Loading branch information
yuriha-chan committed Dec 8, 2023
1 parent 42c9885 commit 399a03a
Show file tree
Hide file tree
Showing 19 changed files with 217 additions and 7 deletions.
8 changes: 8 additions & 0 deletions locales/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ blockedInstances: "Blocked Instances"
blockedInstancesDescription: "List the hostnames of the instances you want to block separated by linebreaks. Listed instances will no longer be able to communicate with this instance."
silencedInstances: "Silenced instances"
silencedInstancesDescription: "List the hostnames of the instances that you want to silence. All accounts of the listed instances will be treated as silenced, can only make follow requests, and cannot mention local accounts if not followed. This will not affect blocked instances."
gtlMutedInstances: "GTL-muted instances"
gtlMutedInstancesDescription: "List the hostnames of the instances that you want to mute in the global timeline. All notes related to the accounts of the listed instances will not be shown up in the global timeline. This will not affect displays in other timelines."
muteAndBlock: "Mutes and Blocks"
mutedUsers: "Muted users"
blockedUsers: "Blocked users"
Expand Down Expand Up @@ -402,6 +404,7 @@ silence: "Silence"
silenceConfirm: "Are you sure that you want to silence this user?"
unsilence: "Undo silencing"
unsilenceConfirm: "Are you sure that you want to undo the silencing of this user?"
gtlMuted: "GTL mutes"
popularUsers: "Popular users"
recentlyUpdatedUsers: "Recently active users"
recentlyRegisteredUsers: "Newly joined users"
Expand Down Expand Up @@ -1750,6 +1753,11 @@ _instanceMute:
instanceMuteDescription2: "Separate with newlines"
title: "Hides notes from listed instances."
heading: "List of instances to be muted"
_instanceGtlMute:
instanceMuteDescription: "This will mute any notes/renotes from the listed instances in the global timeline. In other timelines, these mutings will not take effect."
instanceMuteDescription2: "Separate with newlines"
title: "Hides notes from listed instances."
heading: "List of instances to be muted in the GTL"
_theme:
explore: "Explore Themes"
install: "Install a theme"
Expand Down
9 changes: 9 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ blockedInstances: "ブロックしたサーバー"
blockedInstancesDescription: "ブロックしたいサーバーのホストを改行で区切って設定します。ブロックされたサーバーは、このインスタンスとやり取りできなくなります。"
silencedInstances: "サイレンスしたサーバー"
silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになり、フォロワーでないローカルアカウントにはメンションできなくなります。ブロックしたインスタンスには影響しません。"
gtlMutedInstances: "GTLミュートしたサーバー"
gtlMutedInstancesDescription: "GTLミュートしたいサーバーのホストを改行で区切って設定します。GTLミュートされたサーバーに所属するアカウントに関連する投稿がグローバルタイムラインに表示されなくなります。"
muteAndBlock: "ミュートとブロック"
mutedUsers: "ミュートしたユーザー"
blockedUsers: "ブロックしたユーザー"
Expand Down Expand Up @@ -403,6 +405,7 @@ silence: "サイレンス"
silenceConfirm: "サイレンスしますか?"
unsilence: "サイレンス解除"
unsilenceConfirm: "サイレンス解除しますか?"
gtlMuted: "GTLミュート"
popularUsers: "人気のユーザー"
recentlyUpdatedUsers: "最近投稿したユーザー"
recentlyRegisteredUsers: "最近登録したユーザー"
Expand Down Expand Up @@ -1794,6 +1797,12 @@ _instanceMute:
title: "設定したサーバーのノートを隠します。"
heading: "ミュートするサーバー"

_instanceGtlMute:
instanceMuteDescription: "ミュートしたサーバーからの投稿をグローバルタイムラインに表示しません。その他のタイムラインでは表示されます。"
instanceMuteDescription2: "改行で区切って設定します"
title: "設定したサーバーのノートをグローバルタイムラインから隠します。"
heading: "GTLミュートするサーバー"

_theme:
explore: "テーマを探す"
install: "テーマのインストール"
Expand Down
16 changes: 16 additions & 0 deletions packages/backend/migration/1701920984504-UserInstanceGtlMutings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/

export class UserInstanceGtlMutings1701920984504 {
name = 'UserInstanceGtlMutings1701920984504'

async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" ADD "gtlMutedInstances" jsonb NOT NULL DEFAULT '[]'`);
}

async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "gtlMutedInstances"`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/

export class ServerInstanceGtlMutings1701920984505 {
name = 'ServerInstanceGtlMutings1701920984505'

async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" ADD "gtlMutedHosts" character varying(1024) array NOT NULL DEFAULT '{}'`);
}

async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "gtlMutedHosts"`);
}
}
44 changes: 39 additions & 5 deletions packages/backend/src/core/QueryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export class QueryService {
}

@bindThis
public generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }, exclude?: { id: MiUser['id'] }): void {
public generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: MiUser['id'] }, exclude?: { id: MiUser['id'] }, gtl?: boolean, serverGtlMutedHosts?: string): void {
const mutingQuery = this.mutingsRepository.createQueryBuilder('muting')
.select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: me.id });
Expand All @@ -140,6 +140,13 @@ export class QueryService {
.select('user_profile.mutedInstances')
.where('user_profile.userId = :muterId', { muterId: me.id });

let gtlMutingInstanceQuery;
if (gtl) {
gtlMutingInstanceQuery = this.userProfilesRepository.createQueryBuilder('user_profile')
.select('user_profile.gtlMutedInstances')
.where('user_profile.userId = :muterId', { muterId: me.id });
}

// 投稿の作者をミュートしていない かつ
// 投稿の返信先の作者をミュートしていない かつ
// 投稿の引用元の作者をミュートしていない
Expand All @@ -158,22 +165,49 @@ export class QueryService {
// mute instances
.andWhere(new Brackets(qb => {
qb
.andWhere('note.userHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`);
.where('note.userHost IS NULL')
.orWhere(new Brackets(qbb => {
qbb.where(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`);
if (gtl) {
qbb.andWhere(`NOT ((${ gtlMutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`);
if (serverGtlMutedHosts.length > 0) {
qbb.andWhere(`NOT (note.userHost IN (:...serverGtlMutedHosts))`, { serverGtlMutedHosts });
}
}
}));
}))
.andWhere(new Brackets(qb => {
qb
.where('note.replyUserHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`);
.orWhere(new Brackets(qbb => {
qbb.where(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`);
if (gtl) {
qbb.andWhere(`NOT ((${ gtlMutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`);
if (serverGtlMutedHosts.length > 0) {
qbb.andWhere(`NOT (note.replyUserHost IN (:...serverGtlMutedHosts))`, { serverGtlMutedHosts });
}
}
}));
}))
.andWhere(new Brackets(qb => {
qb
.where('note.renoteUserHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
.orWhere(new Brackets(qbb => {
qbb.where(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
if (gtl) {
qbb.andWhere(`NOT ((${ gtlMutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
if (serverGtlMutedHosts.length > 0) {
qbb.andWhere(`NOT (note.renoteUserHost IN (:...serverGtlMutedHosts))`, { serverGtlMutedHosts });
}
}
}));
}));

q.setParameters(mutingQuery.getParameters());
q.setParameters(mutingInstanceQuery.getParameters());
if (gtl) {
q.setParameters(gtlMutingInstanceQuery.getParameters());
}
}

@bindThis
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/core/entities/UserEntityService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,7 @@ export class UserEntityService implements OnModuleInit {
mutedWords: profile!.mutedWords,
hardMutedWords: profile!.hardMutedWords,
mutedInstances: profile!.mutedInstances,
gtlMutedInstances: profile!.gtlMutedInstances,
mutingNotificationTypes: [], // 後方互換性のため
notificationRecieveConfig: profile!.notificationRecieveConfig,
emailNotificationTypes: profile!.emailNotificationTypes,
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/models/Meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ export class MiMeta {
})
public silencedHosts: string[];

@Column('varchar', {
length: 1024, array: true, default: '{}',
})
public gtlMutedHosts: string[];

@Column('varchar', {
length: 1024,
nullable: true,
Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/models/UserProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,12 @@ export class MiUserProfile {
})
public mutedInstances: string[];

@Column('jsonb', {
default: [],
comment: 'List of instances GTL muted by the user.',
})
public gtlMutedInstances: string[];

@Column('jsonb', {
default: {},
})
Expand Down
8 changes: 8 additions & 0 deletions packages/backend/src/models/json-schema/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,14 @@ export const packedMeDetailedOnlySchema = {
nullable: false, optional: false,
},
},
gtlMutedInstances: {
type: 'array',
nullable: true, optional: false,
items: {
type: 'string',
nullable: false, optional: false,
},
},
notificationRecieveConfig: {
type: 'object',
nullable: false, optional: false,
Expand Down
11 changes: 11 additions & 0 deletions packages/backend/src/server/api/endpoints/admin/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,16 @@ export const meta = {
nullable: false,
},
},
gtlMutedHosts: {
type: 'array',
optional: true,
nullable: false,
items: {
type: 'string',
optional: false,
nullable: false,
},
},
pinnedUsers: {
type: 'array',
optional: false, nullable: false,
Expand Down Expand Up @@ -470,6 +480,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
hiddenTags: instance.hiddenTags,
blockedHosts: instance.blockedHosts,
silencedHosts: instance.silencedHosts,
gtlMutedHosts: instance.gtlMutedHosts,
sensitiveWords: instance.sensitiveWords,
preservedUsernames: instance.preservedUsernames,
hcaptchaSecretKey: instance.hcaptchaSecretKey,
Expand Down
15 changes: 15 additions & 0 deletions packages/backend/src/server/api/endpoints/admin/update-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,13 @@ export const paramDef = {
type: 'string',
},
},
gtlMutedHosts: {
type: 'array',
nullable: true,
items: {
type: 'string',
},
},
},
required: [],
} as const;
Expand Down Expand Up @@ -176,6 +183,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return h !== '' && h !== lv && !set.blockedHosts?.includes(h);
});
}
if (Array.isArray(ps.gtlMutedHosts)) {
let lastValue = '';
set.gtlMutedHosts = ps.gtlMutedHosts.sort().filter((h) => {
const lv = lastValue;
lastValue = h;
return h !== '' && h !== lv && !set.blockedHosts?.includes(h);
});
}
if (ps.themeColor !== undefined) {
set.themeColor = ps.themeColor;
}
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/server/api/endpoints/i/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ export const paramDef = {
mutedInstances: { type: 'array', items: {
type: 'string',
} },
gtlMutedInstances: { type: 'array', items: {
type: 'string',
} },
notificationRecieveConfig: { type: 'object' },
emailNotificationTypes: { type: 'array', items: {
type: 'string',
Expand Down Expand Up @@ -277,6 +280,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
profileUpdates.hardMutedWords = ps.hardMutedWords;
}
if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances;
if (ps.gtlMutedInstances !== undefined) profileUpdates.gtlMutedInstances = ps.gtlMutedInstances;
if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig;
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { MetaService } from '@/core/MetaService.js';
import { ApiError } from '../../error.js';

export const meta = {
Expand Down Expand Up @@ -61,6 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
private roleService: RoleService,
private activeUsersChart: ActiveUsersChart,
private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const policies = await this.roleService.getUserPolicies(me ? me.id : null);
Expand All @@ -80,7 +82,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser');

if (me) {
this.queryService.generateMutedUserQuery(query, me);
const metaInfo = await metaService.fetch();
this.queryService.generateMutedUserQuery(query, me, undefined, true, metaInfo.gtlMutedHosts);
this.queryService.generateBlockedUserQuery(query, me);
this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { checkWordMute } from '@/misc/check-word-mute.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import type { Packed } from '@/misc/json-schema.js';
import type { Meta } from '@/models/Meta.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
Expand All @@ -21,6 +22,7 @@ class GlobalTimelineChannel extends Channel {
private withRenotes: boolean;
private withHashtags: boolean;
private withFiles: boolean;
private meta: Meta;

constructor(
private metaService: MetaService,
Expand All @@ -42,6 +44,7 @@ class GlobalTimelineChannel extends Channel {
this.withRenotes = params.withRenotes ?? true;
this.withHashtags = params.withHashtags ?? true;
this.withFiles = params.withFiles ?? false;
this.meta = await this.metaService.fetch();

// Subscribe events
this.subscriber.on('notesStream', this.onNote);
Expand All @@ -67,6 +70,9 @@ class GlobalTimelineChannel extends Channel {

// Ignore notes from instances the user has muted
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
if (isInstanceMuted(note, new Set<string>(this.userProfile?.gtlMutedInstances ?? []))) return;
// Ignore notes from instances this server has muted
if (isInstanceMuted(note, new Set<string>(this.meta.gtlMutedHosts ?? []))) return;

// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
Expand Down
11 changes: 11 additions & 0 deletions packages/frontend/src/pages/admin/instance-block.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<span>{{ i18n.ts.silencedInstances }}</span>
<template #caption>{{ i18n.ts.silencedInstancesDescription }}</template>
</MkTextarea>
<MkTextarea v-else-if="tab === 'gtlMuted'" v-model="gtlMutedHosts" class="_formBlock">
<span>{{ i18n.ts.gtlMutedInstances }}</span>
<template #caption>{{ i18n.ts.gtlMutedInstancesDescription }}</template>
</MkTextarea>
<MkButton primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</FormSuspense>
</MkSpacer>
Expand All @@ -34,18 +38,21 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';

let blockedHosts: string = $ref('');
let silencedHosts: string = $ref('');
let gtlMutedHosts: string = $ref('');
let tab = $ref('block');

async function init() {
const meta = await os.api('admin/meta');
blockedHosts = meta.blockedHosts.join('\n');
silencedHosts = meta.silencedHosts.join('\n');
gtlMutedHosts = meta.gtlMutedHosts.join('\n');
}

function save() {
os.apiWithDialog('admin/update-meta', {
blockedHosts: blockedHosts.split('\n') || [],
silencedHosts: silencedHosts.split('\n') || [],
gtlMutedHosts: gtlMutedHosts.split('\n') || [],

}).then(() => {
fetchInstance();
Expand All @@ -62,6 +69,10 @@ const headerTabs = $computed(() => [{
key: 'silence',
title: i18n.ts.silence,
icon: 'ti ti-eye-off',
}, {
key: 'gtlMuted',
title: i18n.ts.gtlMuted,
icon: 'ti ti-eye-off',
}]);

definePageMetadata({
Expand Down
Loading

0 comments on commit 399a03a

Please sign in to comment.