merge: timeline changes, new options, silence users (#97)
This commit is contained in:
commit
8debea7161
35 changed files with 280 additions and 14 deletions
|
@ -1101,6 +1101,10 @@ additionalEmojiDictionary: "Additional emoji dictionaries"
|
||||||
installed: "Installed"
|
installed: "Installed"
|
||||||
branding: "Branding"
|
branding: "Branding"
|
||||||
enableServerMachineStats: "Publish server hardware stats"
|
enableServerMachineStats: "Publish server hardware stats"
|
||||||
|
enableAchievements: "Enable Achievements"
|
||||||
|
turnOffAchievements: "Turning this off will disable the achievement system"
|
||||||
|
enableBotTrending: "Populate Hashtags with Bots"
|
||||||
|
turnOffBotTrending: "Turning this off will stop Bots from populating Hashtags"
|
||||||
enableIdenticonGeneration: "Enable user identicon generation"
|
enableIdenticonGeneration: "Enable user identicon generation"
|
||||||
turnOffToImprovePerformance: "Turning this off can increase performance."
|
turnOffToImprovePerformance: "Turning this off can increase performance."
|
||||||
createInviteCode: "Generate invite"
|
createInviteCode: "Generate invite"
|
||||||
|
@ -1137,6 +1141,7 @@ loadConversation: "Show conversation"
|
||||||
pinnedList: "Pinned list"
|
pinnedList: "Pinned list"
|
||||||
keepScreenOn: "Keep screen on"
|
keepScreenOn: "Keep screen on"
|
||||||
clickToOpen: "Click to open notes"
|
clickToOpen: "Click to open notes"
|
||||||
|
showBots: "Show bots in timeline"
|
||||||
verifiedLink: "Link ownership has been verified"
|
verifiedLink: "Link ownership has been verified"
|
||||||
notifyNotes: "Notify about new notes"
|
notifyNotes: "Notify about new notes"
|
||||||
unnotifyNotes: "Stop notifying about new notes"
|
unnotifyNotes: "Stop notifying about new notes"
|
||||||
|
|
5
locales/index.d.ts
vendored
5
locales/index.d.ts
vendored
|
@ -1099,6 +1099,10 @@ export interface Locale {
|
||||||
"installed": string;
|
"installed": string;
|
||||||
"branding": string;
|
"branding": string;
|
||||||
"enableServerMachineStats": string;
|
"enableServerMachineStats": string;
|
||||||
|
"enableAchievements": string;
|
||||||
|
"turnOffAchievements": string;
|
||||||
|
"enableBotTrending": string;
|
||||||
|
"turnOffBotTrending": string;
|
||||||
"enableIdenticonGeneration": string;
|
"enableIdenticonGeneration": string;
|
||||||
"turnOffToImprovePerformance": string;
|
"turnOffToImprovePerformance": string;
|
||||||
"createInviteCode": string;
|
"createInviteCode": string;
|
||||||
|
@ -1135,6 +1139,7 @@ export interface Locale {
|
||||||
"pinnedList": string;
|
"pinnedList": string;
|
||||||
"keepScreenOn": string;
|
"keepScreenOn": string;
|
||||||
"clickToOpen": string;
|
"clickToOpen": string;
|
||||||
|
"showBots": string;
|
||||||
"verifiedLink": string;
|
"verifiedLink": string;
|
||||||
"notifyNotes": string;
|
"notifyNotes": string;
|
||||||
"unnotifyNotes": string;
|
"unnotifyNotes": string;
|
||||||
|
|
|
@ -1096,6 +1096,10 @@ additionalEmojiDictionary: "絵文字の追加辞書"
|
||||||
installed: "インストール済み"
|
installed: "インストール済み"
|
||||||
branding: "ブランディング"
|
branding: "ブランディング"
|
||||||
enableServerMachineStats: "サーバーのマシン情報を公開する"
|
enableServerMachineStats: "サーバーのマシン情報を公開する"
|
||||||
|
enableAchievements: "実績を有効にする"
|
||||||
|
turnOffAchievements: "これをオフにすると、達成システムは無効になります。"
|
||||||
|
enableBotTrending: "ハッシュタグにボットを追加する"
|
||||||
|
turnOffBotTrending: "これをオフにすると、ボットがハッシュタグを入力しなくなります。"
|
||||||
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
|
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
|
||||||
turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。"
|
turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。"
|
||||||
createInviteCode: "招待コードを作成"
|
createInviteCode: "招待コードを作成"
|
||||||
|
@ -1132,6 +1136,7 @@ loadConversation: "会話を見る"
|
||||||
pinnedList: "ピン留めされたリスト"
|
pinnedList: "ピン留めされたリスト"
|
||||||
keepScreenOn: "デバイスの画面を常にオンにする"
|
keepScreenOn: "デバイスの画面を常にオンにする"
|
||||||
clickToOpen: "クリックしてノートを開く"
|
clickToOpen: "クリックしてノートを開く"
|
||||||
|
showBots: "ボットをタイムラインに表示"
|
||||||
verifiedLink: "このリンク先の所有者であることが確認されました"
|
verifiedLink: "このリンク先の所有者であることが確認されました"
|
||||||
notifyNotes: "投稿を通知"
|
notifyNotes: "投稿を通知"
|
||||||
unnotifyNotes: "投稿の通知を解除"
|
unnotifyNotes: "投稿の通知を解除"
|
||||||
|
|
16
packages/backend/migration/1697603945000-BotTrending.js
Normal file
16
packages/backend/migration/1697603945000-BotTrending.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class BotTrending1697603945000 {
|
||||||
|
name = 'BotTrending1697603945000'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "enableBotTrending" boolean NOT NULL DEFAULT true`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableBotTrending"`);
|
||||||
|
}
|
||||||
|
}
|
16
packages/backend/migration/1697624010000-isSilenced.js
Normal file
16
packages/backend/migration/1697624010000-isSilenced.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class IsSilenced1697624010000 {
|
||||||
|
name = 'IsSilenced1697624010000'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "isSilenced" boolean NOT NULL DEFAULT false`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isSilenced"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -511,7 +511,11 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||||
|
|
||||||
// ハッシュタグ更新
|
// ハッシュタグ更新
|
||||||
if (data.visibility === 'public' || data.visibility === 'home') {
|
if (data.visibility === 'public' || data.visibility === 'home') {
|
||||||
|
if (user.isBot && meta.enableBotTrending) {
|
||||||
this.hashtagService.updateHashtags(user, tags);
|
this.hashtagService.updateHashtags(user, tags);
|
||||||
|
} else if (!user.isBot) {
|
||||||
|
this.hashtagService.updateHashtags(user, tags);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment notes count (user)
|
// Increment notes count (user)
|
||||||
|
|
|
@ -368,6 +368,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
createdAt: this.idService.parse(user.id).date.toISOString(),
|
createdAt: this.idService.parse(user.id).date.toISOString(),
|
||||||
isBot: user.isBot ?? falsy,
|
isBot: user.isBot ?? falsy,
|
||||||
isCat: user.isCat ?? falsy,
|
isCat: user.isCat ?? falsy,
|
||||||
|
isSilenced: user.isSilenced || this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
|
||||||
speakAsCat: user.speakAsCat ?? falsy,
|
speakAsCat: user.speakAsCat ?? falsy,
|
||||||
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
|
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
|
||||||
name: instance.name,
|
name: instance.name,
|
||||||
|
@ -404,7 +405,6 @@ export class UserEntityService implements OnModuleInit {
|
||||||
backgroundUrl: user.backgroundUrl,
|
backgroundUrl: user.backgroundUrl,
|
||||||
backgroundBlurhash: user.backgroundBlurhash,
|
backgroundBlurhash: user.backgroundBlurhash,
|
||||||
isLocked: user.isLocked,
|
isLocked: user.isLocked,
|
||||||
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
|
|
||||||
isSuspended: user.isSuspended ?? falsy,
|
isSuspended: user.isSuspended ?? falsy,
|
||||||
location: profile!.location,
|
location: profile!.location,
|
||||||
birthday: profile!.birthday,
|
birthday: profile!.birthday,
|
||||||
|
|
|
@ -247,6 +247,11 @@ export class MiMeta {
|
||||||
})
|
})
|
||||||
public enableSensitiveMediaDetectionForVideos: boolean;
|
public enableSensitiveMediaDetectionForVideos: boolean;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
public enableBotTrending: boolean;
|
||||||
|
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 1024,
|
length: 1024,
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
|
|
@ -173,6 +173,12 @@ export class MiUser {
|
||||||
})
|
})
|
||||||
public isSuspended: boolean;
|
public isSuspended: boolean;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: false,
|
||||||
|
comment: 'Whether the User is silenced.',
|
||||||
|
})
|
||||||
|
public isSilenced: boolean;
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
comment: 'Whether the User is locked.',
|
comment: 'Whether the User is locked.',
|
||||||
|
|
|
@ -47,6 +47,10 @@ export const packedUserLiteSchema = {
|
||||||
nullable: false, optional: true,
|
nullable: false, optional: true,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
isSilenced: {
|
||||||
|
type: 'boolean',
|
||||||
|
nullable: false, optional: false,
|
||||||
|
},
|
||||||
isBot: {
|
isBot: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: true,
|
nullable: false, optional: true,
|
||||||
|
@ -135,10 +139,6 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
},
|
},
|
||||||
isSilenced: {
|
|
||||||
type: 'boolean',
|
|
||||||
nullable: false, optional: false,
|
|
||||||
},
|
|
||||||
isSuspended: {
|
isSuspended: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: false, optional: false,
|
nullable: false, optional: false,
|
||||||
|
|
|
@ -63,6 +63,8 @@ import * as ep___admin_showUser from './endpoints/admin/show-user.js';
|
||||||
import * as ep___admin_showUsers from './endpoints/admin/show-users.js';
|
import * as ep___admin_showUsers from './endpoints/admin/show-users.js';
|
||||||
import * as ep___admin_nsfwUser from './endpoints/admin/nsfw-user.js';
|
import * as ep___admin_nsfwUser from './endpoints/admin/nsfw-user.js';
|
||||||
import * as ep___admin_unnsfwUser from './endpoints/admin/unnsfw-user.js';
|
import * as ep___admin_unnsfwUser from './endpoints/admin/unnsfw-user.js';
|
||||||
|
import * as ep___admin_silenceUser from './endpoints/admin/silence-user.js';
|
||||||
|
import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js';
|
||||||
import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
|
import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
|
||||||
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
|
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
|
||||||
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
|
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
|
||||||
|
@ -418,6 +420,8 @@ const $admin_showUser: Provider = { provide: 'ep:admin/show-user', useClass: ep_
|
||||||
const $admin_showUsers: Provider = { provide: 'ep:admin/show-users', useClass: ep___admin_showUsers.default };
|
const $admin_showUsers: Provider = { provide: 'ep:admin/show-users', useClass: ep___admin_showUsers.default };
|
||||||
const $admin_nsfwUser: Provider = { provide: 'ep:admin/nsfw-user', useClass: ep___admin_nsfwUser.default };
|
const $admin_nsfwUser: Provider = { provide: 'ep:admin/nsfw-user', useClass: ep___admin_nsfwUser.default };
|
||||||
const $admin_unnsfwUser: Provider = { provide: 'ep:admin/unnsfw-user', useClass: ep___admin_unnsfwUser.default };
|
const $admin_unnsfwUser: Provider = { provide: 'ep:admin/unnsfw-user', useClass: ep___admin_unnsfwUser.default };
|
||||||
|
const $admin_silenceUser: Provider = { provide: 'ep:admin/silence-user', useClass: ep___admin_silenceUser.default };
|
||||||
|
const $admin_unsilenceUser: Provider = { provide: 'ep:admin/unsilence-user', useClass: ep___admin_unsilenceUser.default };
|
||||||
const $admin_suspendUser: Provider = { provide: 'ep:admin/suspend-user', useClass: ep___admin_suspendUser.default };
|
const $admin_suspendUser: Provider = { provide: 'ep:admin/suspend-user', useClass: ep___admin_suspendUser.default };
|
||||||
const $admin_unsuspendUser: Provider = { provide: 'ep:admin/unsuspend-user', useClass: ep___admin_unsuspendUser.default };
|
const $admin_unsuspendUser: Provider = { provide: 'ep:admin/unsuspend-user', useClass: ep___admin_unsuspendUser.default };
|
||||||
const $admin_updateMeta: Provider = { provide: 'ep:admin/update-meta', useClass: ep___admin_updateMeta.default };
|
const $admin_updateMeta: Provider = { provide: 'ep:admin/update-meta', useClass: ep___admin_updateMeta.default };
|
||||||
|
@ -777,6 +781,8 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
|
||||||
$admin_showUsers,
|
$admin_showUsers,
|
||||||
$admin_nsfwUser,
|
$admin_nsfwUser,
|
||||||
$admin_unnsfwUser,
|
$admin_unnsfwUser,
|
||||||
|
$admin_silenceUser,
|
||||||
|
$admin_unsilenceUser,
|
||||||
$admin_suspendUser,
|
$admin_suspendUser,
|
||||||
$admin_unsuspendUser,
|
$admin_unsuspendUser,
|
||||||
$admin_updateMeta,
|
$admin_updateMeta,
|
||||||
|
@ -1130,6 +1136,8 @@ const $sponsors: Provider = { provide: 'ep:sponsors', useClass: ep___sponsors.de
|
||||||
$admin_showUsers,
|
$admin_showUsers,
|
||||||
$admin_nsfwUser,
|
$admin_nsfwUser,
|
||||||
$admin_unnsfwUser,
|
$admin_unnsfwUser,
|
||||||
|
$admin_silenceUser,
|
||||||
|
$admin_unsilenceUser,
|
||||||
$admin_suspendUser,
|
$admin_suspendUser,
|
||||||
$admin_unsuspendUser,
|
$admin_unsuspendUser,
|
||||||
$admin_updateMeta,
|
$admin_updateMeta,
|
||||||
|
|
|
@ -63,6 +63,8 @@ import * as ep___admin_showUser from './endpoints/admin/show-user.js';
|
||||||
import * as ep___admin_showUsers from './endpoints/admin/show-users.js';
|
import * as ep___admin_showUsers from './endpoints/admin/show-users.js';
|
||||||
import * as ep___admin_nsfwUser from './endpoints/admin/nsfw-user.js';
|
import * as ep___admin_nsfwUser from './endpoints/admin/nsfw-user.js';
|
||||||
import * as ep___admin_unnsfwUser from './endpoints/admin/unnsfw-user.js';
|
import * as ep___admin_unnsfwUser from './endpoints/admin/unnsfw-user.js';
|
||||||
|
import * as ep___admin_silenceUser from './endpoints/admin/silence-user.js';
|
||||||
|
import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js';
|
||||||
import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
|
import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
|
||||||
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
|
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
|
||||||
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
|
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
|
||||||
|
@ -416,6 +418,8 @@ const eps = [
|
||||||
['admin/show-users', ep___admin_showUsers],
|
['admin/show-users', ep___admin_showUsers],
|
||||||
['admin/nsfw-user', ep___admin_nsfwUser],
|
['admin/nsfw-user', ep___admin_nsfwUser],
|
||||||
['admin/unnsfw-user', ep___admin_unnsfwUser],
|
['admin/unnsfw-user', ep___admin_unnsfwUser],
|
||||||
|
['admin/silence-user', ep___admin_silenceUser],
|
||||||
|
['admin/unsilence-user', ep___admin_unsilenceUser],
|
||||||
['admin/suspend-user', ep___admin_suspendUser],
|
['admin/suspend-user', ep___admin_suspendUser],
|
||||||
['admin/unsuspend-user', ep___admin_unsuspendUser],
|
['admin/unsuspend-user', ep___admin_unsuspendUser],
|
||||||
['admin/update-meta', ep___admin_updateMeta],
|
['admin/update-meta', ep___admin_updateMeta],
|
||||||
|
|
|
@ -178,6 +178,10 @@ export const meta = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
enableBotTrending: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
proxyAccountId: {
|
proxyAccountId: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
|
@ -391,6 +395,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
sensitiveMediaDetectionSensitivity: instance.sensitiveMediaDetectionSensitivity,
|
sensitiveMediaDetectionSensitivity: instance.sensitiveMediaDetectionSensitivity,
|
||||||
setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically,
|
setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically,
|
||||||
enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos,
|
enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos,
|
||||||
|
enableBotTrending: instance.enableBotTrending,
|
||||||
proxyAccountId: instance.proxyAccountId,
|
proxyAccountId: instance.proxyAccountId,
|
||||||
summalyProxy: instance.summalyProxy,
|
summalyProxy: instance.summalyProxy,
|
||||||
email: instance.email,
|
email: instance.email,
|
||||||
|
|
|
@ -58,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
const isModerator = await this.roleService.isModerator(user);
|
const isModerator = await this.roleService.isModerator(user);
|
||||||
const isSilenced = !(await this.roleService.getUserPolicies(user.id)).canPublicNote;
|
const isSilenced = user.isSilenced || !(await this.roleService.getUserPolicies(user.id)).canPublicNote;
|
||||||
|
|
||||||
const _me = await this.usersRepository.findOneByOrFail({ id: me.id });
|
const _me = await this.usersRepository.findOneByOrFail({ id: me.id });
|
||||||
if (!await this.roleService.isAdministrator(_me) && await this.roleService.isAdministrator(user)) {
|
if (!await this.roleService.isAdministrator(_me) && await this.roleService.isAdministrator(user)) {
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import type { UsersRepository } from '@/models/_.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
userId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
|
required: ['userId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
private roleService: RoleService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
throw new Error('user not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await this.roleService.isModerator(user)) {
|
||||||
|
throw new Error('cannot silence moderator account');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.usersRepository.update(user.id, {
|
||||||
|
isSilenced: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import type { UsersRepository } from '@/models/_.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
userId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
|
required: ['userId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
throw new Error('user not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.usersRepository.update(user.id, {
|
||||||
|
isSilenced: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -72,6 +72,7 @@ export const paramDef = {
|
||||||
sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
|
sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
|
||||||
setSensitiveFlagAutomatically: { type: 'boolean' },
|
setSensitiveFlagAutomatically: { type: 'boolean' },
|
||||||
enableSensitiveMediaDetectionForVideos: { type: 'boolean' },
|
enableSensitiveMediaDetectionForVideos: { type: 'boolean' },
|
||||||
|
enableBotTrending: { type: 'boolean' },
|
||||||
proxyAccountId: { type: 'string', format: 'misskey:id', nullable: true },
|
proxyAccountId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
maintainerName: { type: 'string', nullable: true },
|
maintainerName: { type: 'string', nullable: true },
|
||||||
maintainerEmail: { type: 'string', nullable: true },
|
maintainerEmail: { type: 'string', nullable: true },
|
||||||
|
@ -301,6 +302,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
set.enableSensitiveMediaDetectionForVideos = ps.enableSensitiveMediaDetectionForVideos;
|
set.enableSensitiveMediaDetectionForVideos = ps.enableSensitiveMediaDetectionForVideos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.enableBotTrending !== undefined) {
|
||||||
|
set.enableBotTrending = ps.enableBotTrending;
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.proxyAccountId !== undefined) {
|
if (ps.proxyAccountId !== undefined) {
|
||||||
set.proxyAccountId = ps.proxyAccountId;
|
set.proxyAccountId = ps.proxyAccountId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { RoleService } from '@/core/RoleService.js';
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notes'],
|
tags: ['notes'],
|
||||||
|
@ -40,6 +41,7 @@ export const paramDef = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
withFiles: { type: 'boolean', default: false },
|
withFiles: { type: 'boolean', default: false },
|
||||||
|
withBots: { type: 'boolean', default: true },
|
||||||
withRenotes: { type: 'boolean', default: true },
|
withRenotes: { type: 'boolean', default: true },
|
||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
sinceId: { type: 'string', format: 'misskey:id' },
|
sinceId: { type: 'string', format: 'misskey:id' },
|
||||||
|
@ -60,6 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
private activeUsersChart: ActiveUsersChart,
|
private activeUsersChart: ActiveUsersChart,
|
||||||
|
private cacheService: CacheService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const policies = await this.roleService.getUserPolicies(me ? me.id : null);
|
const policies = await this.roleService.getUserPolicies(me ? me.id : null);
|
||||||
|
@ -67,6 +70,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
throw new ApiError(meta.errors.gtlDisabled);
|
throw new ApiError(meta.errors.gtlDisabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [
|
||||||
|
followings,
|
||||||
|
] = me ? await Promise.all([
|
||||||
|
this.cacheService.userFollowingsCache.fetch(me.id),
|
||||||
|
]) : [undefined];
|
||||||
|
|
||||||
//#region Construct query
|
//#region Construct query
|
||||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
|
||||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||||
|
@ -87,9 +96,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (ps.withFiles) {
|
if (ps.withFiles) {
|
||||||
query.andWhere('note.fileIds != \'{}\'');
|
query.andWhere('note.fileIds != \'{}\'');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!ps.withBots) query.andWhere('user.isBot = FALSE');
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
const timeline = await query.limit(ps.limit).getMany();
|
let timeline = await query.limit(ps.limit).getMany();
|
||||||
|
|
||||||
|
timeline = timeline.filter(note => {
|
||||||
|
if (note.user?.isSilenced && me && followings && note.userId !== me.id && !followings[note.userId]) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
process.nextTick(() => {
|
process.nextTick(() => {
|
||||||
if (me) {
|
if (me) {
|
||||||
|
|
|
@ -56,6 +56,7 @@ export const paramDef = {
|
||||||
withFiles: { type: 'boolean', default: false },
|
withFiles: { type: 'boolean', default: false },
|
||||||
withRenotes: { type: 'boolean', default: true },
|
withRenotes: { type: 'boolean', default: true },
|
||||||
withReplies: { type: 'boolean', default: false },
|
withReplies: { type: 'boolean', default: false },
|
||||||
|
withBots: { type: 'boolean', default: true },
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -86,10 +87,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
const [
|
const [
|
||||||
|
followings,
|
||||||
userIdsWhoMeMuting,
|
userIdsWhoMeMuting,
|
||||||
userIdsWhoMeMutingRenotes,
|
userIdsWhoMeMutingRenotes,
|
||||||
userIdsWhoBlockingMe,
|
userIdsWhoBlockingMe,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
|
this.cacheService.userFollowingsCache.fetch(me.id),
|
||||||
this.cacheService.userMutingsCache.fetch(me.id),
|
this.cacheService.userMutingsCache.fetch(me.id),
|
||||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||||
this.cacheService.userBlockedCache.fetch(me.id),
|
this.cacheService.userBlockedCache.fetch(me.id),
|
||||||
|
@ -134,6 +137,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
.leftJoinAndSelect('note.channel', 'channel');
|
.leftJoinAndSelect('note.channel', 'channel');
|
||||||
|
|
||||||
|
if (!ps.withBots) query.andWhere('user.isBot = FALSE');
|
||||||
|
|
||||||
let timeline = await query.getMany();
|
let timeline = await query.getMany();
|
||||||
|
|
||||||
timeline = timeline.filter(note => {
|
timeline = timeline.filter(note => {
|
||||||
|
@ -148,6 +153,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (ps.withRenotes === false) return false;
|
if (ps.withRenotes === false) return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (note.user?.isSilenced && note.userId !== me.id && !followings[note.userId]) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
|
@ -46,6 +46,7 @@ export const paramDef = {
|
||||||
withFiles: { type: 'boolean', default: false },
|
withFiles: { type: 'boolean', default: false },
|
||||||
withRenotes: { type: 'boolean', default: true },
|
withRenotes: { type: 'boolean', default: true },
|
||||||
withReplies: { type: 'boolean', default: false },
|
withReplies: { type: 'boolean', default: false },
|
||||||
|
withBots: { type: 'boolean', default: true },
|
||||||
excludeNsfw: { type: 'boolean', default: false },
|
excludeNsfw: { type: 'boolean', default: false },
|
||||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||||
sinceId: { type: 'string', format: 'misskey:id' },
|
sinceId: { type: 'string', format: 'misskey:id' },
|
||||||
|
@ -82,14 +83,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
|
|
||||||
const [
|
const [
|
||||||
|
followings,
|
||||||
userIdsWhoMeMuting,
|
userIdsWhoMeMuting,
|
||||||
userIdsWhoMeMutingRenotes,
|
userIdsWhoMeMutingRenotes,
|
||||||
userIdsWhoBlockingMe,
|
userIdsWhoBlockingMe,
|
||||||
] = me ? await Promise.all([
|
] = me ? await Promise.all([
|
||||||
|
this.cacheService.userFollowingsCache.fetch(me.id),
|
||||||
this.cacheService.userMutingsCache.fetch(me.id),
|
this.cacheService.userMutingsCache.fetch(me.id),
|
||||||
this.cacheService.renoteMutingsCache.fetch(me.id),
|
this.cacheService.renoteMutingsCache.fetch(me.id),
|
||||||
this.cacheService.userBlockedCache.fetch(me.id),
|
this.cacheService.userBlockedCache.fetch(me.id),
|
||||||
]) : [new Set<string>(), new Set<string>(), new Set<string>()];
|
]) : [undefined, new Set<string>(), new Set<string>(), new Set<string>()];
|
||||||
|
|
||||||
let noteIds: string[];
|
let noteIds: string[];
|
||||||
|
|
||||||
|
@ -119,6 +122,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
.leftJoinAndSelect('note.channel', 'channel');
|
.leftJoinAndSelect('note.channel', 'channel');
|
||||||
|
|
||||||
|
if (!ps.withBots) query.andWhere('user.isBot = FALSE');
|
||||||
|
|
||||||
let timeline = await query.getMany();
|
let timeline = await query.getMany();
|
||||||
|
|
||||||
timeline = timeline.filter(note => {
|
timeline = timeline.filter(note => {
|
||||||
|
@ -134,6 +139,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (ps.withRenotes === false) return false;
|
if (ps.withRenotes === false) return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (note.user?.isSilenced && me && followings && note.userId !== me.id && !followings[note.userId]) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { QueryService } from '@/core/QueryService.js';
|
import { QueryService } from '@/core/QueryService.js';
|
||||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['notes', 'hashtags'],
|
tags: ['notes', 'hashtags'],
|
||||||
|
@ -71,6 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
|
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
private queryService: QueryService,
|
private queryService: QueryService,
|
||||||
|
private metaService: MetaService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||||
|
@ -80,6 +82,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.leftJoinAndSelect('reply.user', 'replyUser')
|
.leftJoinAndSelect('reply.user', 'replyUser')
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser');
|
.leftJoinAndSelect('renote.user', 'renoteUser');
|
||||||
|
|
||||||
|
const meta = await this.metaService.fetch(true);
|
||||||
|
|
||||||
|
if (!meta.enableBotTrending) query.andWhere('user.isBot = FALSE');
|
||||||
|
|
||||||
this.queryService.generateVisibilityQuery(query, me);
|
this.queryService.generateVisibilityQuery(query, me);
|
||||||
if (me) this.queryService.generateMutedUserQuery(query, me);
|
if (me) this.queryService.generateMutedUserQuery(query, me);
|
||||||
if (me) this.queryService.generateBlockedUserQuery(query, me);
|
if (me) this.queryService.generateBlockedUserQuery(query, me);
|
||||||
|
|
|
@ -46,6 +46,7 @@ export const paramDef = {
|
||||||
includeLocalRenotes: { type: 'boolean', default: true },
|
includeLocalRenotes: { type: 'boolean', default: true },
|
||||||
withFiles: { type: 'boolean', default: false },
|
withFiles: { type: 'boolean', default: false },
|
||||||
withRenotes: { type: 'boolean', default: true },
|
withRenotes: { type: 'boolean', default: true },
|
||||||
|
withBots: { type: 'boolean', default: true },
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -97,6 +98,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||||
.leftJoinAndSelect('note.channel', 'channel');
|
.leftJoinAndSelect('note.channel', 'channel');
|
||||||
|
|
||||||
|
if (!ps.withBots) query.andWhere('user.isBot = FALSE');
|
||||||
|
|
||||||
let timeline = await query.getMany();
|
let timeline = await query.getMany();
|
||||||
|
|
||||||
timeline = timeline.filter(note => {
|
timeline = timeline.filter(note => {
|
||||||
|
@ -114,6 +117,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (note.reply && note.reply.visibility === 'followers') {
|
if (note.reply && note.reply.visibility === 'followers') {
|
||||||
if (!Object.hasOwn(followings, note.reply.userId)) return false;
|
if (!Object.hasOwn(followings, note.reply.userId)) return false;
|
||||||
}
|
}
|
||||||
|
if (note.user?.isSilenced && note.userId !== me.id && !followings[note.userId]) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,6 +20,7 @@ class GlobalTimelineChannel extends Channel {
|
||||||
public static requireCredential = false;
|
public static requireCredential = false;
|
||||||
private withRenotes: boolean;
|
private withRenotes: boolean;
|
||||||
private withFiles: boolean;
|
private withFiles: boolean;
|
||||||
|
private withBots: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
|
@ -40,6 +41,7 @@ class GlobalTimelineChannel extends Channel {
|
||||||
|
|
||||||
this.withRenotes = params.withRenotes ?? true;
|
this.withRenotes = params.withRenotes ?? true;
|
||||||
this.withFiles = params.withFiles ?? false;
|
this.withFiles = params.withFiles ?? false;
|
||||||
|
this.withBots = params.withBots ?? true;
|
||||||
|
|
||||||
// Subscribe events
|
// Subscribe events
|
||||||
this.subscriber.on('notesStream', this.onNote);
|
this.subscriber.on('notesStream', this.onNote);
|
||||||
|
@ -48,6 +50,7 @@ class GlobalTimelineChannel extends Channel {
|
||||||
@bindThis
|
@bindThis
|
||||||
private async onNote(note: Packed<'Note'>) {
|
private async onNote(note: Packed<'Note'>) {
|
||||||
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
|
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
|
||||||
|
if (!this.withBots && note.user.isBot) return;
|
||||||
|
|
||||||
if (note.visibility !== 'public') return;
|
if (note.visibility !== 'public') return;
|
||||||
if (note.channelId != null) return;
|
if (note.channelId != null) return;
|
||||||
|
@ -59,6 +62,8 @@ class GlobalTimelineChannel extends Channel {
|
||||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (note.user.isSilenced && !this.following[note.userId] && note.userId !== this.user!.id) return;
|
||||||
|
|
||||||
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
||||||
|
|
||||||
// Ignore notes from instances the user has muted
|
// Ignore notes from instances the user has muted
|
||||||
|
|
|
@ -64,6 +64,8 @@ class HomeTimelineChannel extends Channel {
|
||||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (note.user.isSilenced && !this.following[note.userId] && note.userId !== this.user!.id) return;
|
||||||
|
|
||||||
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
|
|
|
@ -20,6 +20,7 @@ class HybridTimelineChannel extends Channel {
|
||||||
public static requireCredential = true;
|
public static requireCredential = true;
|
||||||
private withRenotes: boolean;
|
private withRenotes: boolean;
|
||||||
private withReplies: boolean;
|
private withReplies: boolean;
|
||||||
|
private withBots: boolean;
|
||||||
private withFiles: boolean;
|
private withFiles: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -41,6 +42,7 @@ class HybridTimelineChannel extends Channel {
|
||||||
|
|
||||||
this.withRenotes = params.withRenotes ?? true;
|
this.withRenotes = params.withRenotes ?? true;
|
||||||
this.withReplies = params.withReplies ?? false;
|
this.withReplies = params.withReplies ?? false;
|
||||||
|
this.withBots = params.withBots ?? true;
|
||||||
this.withFiles = params.withFiles ?? false;
|
this.withFiles = params.withFiles ?? false;
|
||||||
|
|
||||||
// Subscribe events
|
// Subscribe events
|
||||||
|
@ -50,6 +52,7 @@ class HybridTimelineChannel extends Channel {
|
||||||
@bindThis
|
@bindThis
|
||||||
private async onNote(note: Packed<'Note'>) {
|
private async onNote(note: Packed<'Note'>) {
|
||||||
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
|
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
|
||||||
|
if (!this.withBots && note.user.isBot) return;
|
||||||
|
|
||||||
// チャンネルの投稿ではなく、自分自身の投稿 または
|
// チャンネルの投稿ではなく、自分自身の投稿 または
|
||||||
// チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または
|
// チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または
|
||||||
|
@ -78,6 +81,8 @@ class HybridTimelineChannel extends Channel {
|
||||||
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (note.user.isSilenced && !this.following[note.userId] && note.userId !== this.user!.id) return;
|
||||||
|
|
||||||
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
|
|
|
@ -19,6 +19,7 @@ class LocalTimelineChannel extends Channel {
|
||||||
public static requireCredential = false;
|
public static requireCredential = false;
|
||||||
private withRenotes: boolean;
|
private withRenotes: boolean;
|
||||||
private withReplies: boolean;
|
private withReplies: boolean;
|
||||||
|
private withBots: boolean;
|
||||||
private withFiles: boolean;
|
private withFiles: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -40,6 +41,7 @@ class LocalTimelineChannel extends Channel {
|
||||||
|
|
||||||
this.withRenotes = params.withRenotes ?? true;
|
this.withRenotes = params.withRenotes ?? true;
|
||||||
this.withReplies = params.withReplies ?? false;
|
this.withReplies = params.withReplies ?? false;
|
||||||
|
this.withBots = params.withBots ?? true;
|
||||||
this.withFiles = params.withFiles ?? false;
|
this.withFiles = params.withFiles ?? false;
|
||||||
|
|
||||||
// Subscribe events
|
// Subscribe events
|
||||||
|
@ -49,6 +51,7 @@ class LocalTimelineChannel extends Channel {
|
||||||
@bindThis
|
@bindThis
|
||||||
private async onNote(note: Packed<'Note'>) {
|
private async onNote(note: Packed<'Note'>) {
|
||||||
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
|
if (this.withFiles && (note.fileIds == null || note.fileIds.length === 0)) return;
|
||||||
|
if (!this.withBots && note.user.isBot) return;
|
||||||
|
|
||||||
if (note.user.host !== null) return;
|
if (note.user.host !== null) return;
|
||||||
if (note.visibility !== 'public') return;
|
if (note.visibility !== 'public') return;
|
||||||
|
@ -61,6 +64,8 @@ class LocalTimelineChannel extends Channel {
|
||||||
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
|
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (note.user.isSilenced && !this.following[note.userId] && note.userId !== this.user!.id) return;
|
||||||
|
|
||||||
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
if (note.renote && note.text == null && (note.fileIds == null || note.fileIds.length === 0) && !this.withRenotes) return;
|
||||||
|
|
||||||
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
|
||||||
|
|
|
@ -25,11 +25,13 @@ const props = withDefaults(defineProps<{
|
||||||
sound?: boolean;
|
sound?: boolean;
|
||||||
withRenotes?: boolean;
|
withRenotes?: boolean;
|
||||||
withReplies?: boolean;
|
withReplies?: boolean;
|
||||||
|
withBots?: boolean;
|
||||||
onlyFiles?: boolean;
|
onlyFiles?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
withRenotes: true,
|
withRenotes: true,
|
||||||
withReplies: false,
|
withReplies: false,
|
||||||
onlyFiles: false,
|
onlyFiles: false,
|
||||||
|
withBots: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -93,11 +95,13 @@ if (props.src === 'antenna') {
|
||||||
query = {
|
query = {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
withReplies: props.withReplies,
|
||||||
|
withBots: props.withBots,
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('localTimeline', {
|
connection = stream.useChannel('localTimeline', {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
withReplies: props.withReplies,
|
||||||
|
withBots: props.withBots,
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
});
|
});
|
||||||
connection.on('note', prepend);
|
connection.on('note', prepend);
|
||||||
|
@ -106,11 +110,13 @@ if (props.src === 'antenna') {
|
||||||
query = {
|
query = {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
withReplies: props.withReplies,
|
||||||
|
withBots: props.withBots,
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('hybridTimeline', {
|
connection = stream.useChannel('hybridTimeline', {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
withReplies: props.withReplies,
|
withReplies: props.withReplies,
|
||||||
|
withBots: props.withBots,
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
});
|
});
|
||||||
connection.on('note', prepend);
|
connection.on('note', prepend);
|
||||||
|
@ -118,10 +124,12 @@ if (props.src === 'antenna') {
|
||||||
endpoint = 'notes/global-timeline';
|
endpoint = 'notes/global-timeline';
|
||||||
query = {
|
query = {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
|
withBots: props.withBots,
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('globalTimeline', {
|
connection = stream.useChannel('globalTimeline', {
|
||||||
withRenotes: props.withRenotes,
|
withRenotes: props.withRenotes,
|
||||||
|
withBots: props.withBots,
|
||||||
withFiles: props.onlyFiles ? true : undefined,
|
withFiles: props.onlyFiles ? true : undefined,
|
||||||
});
|
});
|
||||||
connection.on('note', prepend);
|
connection.on('note', prepend);
|
||||||
|
|
|
@ -76,6 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
|
<MkSwitch v-model="silenced" @update:modelValue="toggleSilence">{{ i18n.ts.silence }}</MkSwitch>
|
||||||
<MkSwitch v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch>
|
<MkSwitch v-model="suspended" @update:modelValue="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch>
|
||||||
<MkSwitch v-model="markedAsNSFW" @update:modelValue="toggleNSFW">{{ i18n.ts.markAsNSFW }}</MkSwitch>
|
<MkSwitch v-model="markedAsNSFW" @update:modelValue="toggleNSFW">{{ i18n.ts.markAsNSFW }}</MkSwitch>
|
||||||
|
|
||||||
|
@ -306,6 +307,19 @@ async function toggleNSFW(v) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleSilence(v) {
|
||||||
|
const confirm = await os.confirm({
|
||||||
|
type: 'warning',
|
||||||
|
text: v ? i18n.ts.silenceConfirm : i18n.ts.unsilenceConfirm,
|
||||||
|
});
|
||||||
|
if (confirm.canceled) {
|
||||||
|
silenced = !v;
|
||||||
|
} else {
|
||||||
|
await os.api(v ? 'admin/silence-user' : 'admin/unsilence-user', { userId: user.id });
|
||||||
|
await refreshUser();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function toggleSuspend(v) {
|
async function toggleSuspend(v) {
|
||||||
const confirm = await os.confirm({
|
const confirm = await os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
|
|
|
@ -18,8 +18,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<div class="_panel" style="padding: 16px;">
|
<div class="_panel" style="padding: 16px;">
|
||||||
<MkSwitch v-model="enableAchievements">
|
<MkSwitch v-model="enableAchievements">
|
||||||
<template #label>Enable Achievements</template>
|
<template #label>{{ i18n.ts.enableAchievements }}</template>
|
||||||
<template #caption>Turning this off will disable the achievement system</template>
|
<template #caption>{{ i18n.ts.turnOffAchievements}}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="_panel" style="padding: 16px;">
|
||||||
|
<MkSwitch v-model="enableBotTrending">
|
||||||
|
<template #label>{{ i18n.ts.enableBotTrending }}</template>
|
||||||
|
<template #caption>{{ i18n.ts.turnOffBotTrending }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -61,6 +68,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
|
|
||||||
let enableServerMachineStats: boolean = $ref(false);
|
let enableServerMachineStats: boolean = $ref(false);
|
||||||
let enableAchievements: boolean = $ref(false);
|
let enableAchievements: boolean = $ref(false);
|
||||||
|
let enableBotTrending: boolean = $ref(false);
|
||||||
let enableIdenticonGeneration: boolean = $ref(false);
|
let enableIdenticonGeneration: boolean = $ref(false);
|
||||||
let enableChartsForRemoteUser: boolean = $ref(false);
|
let enableChartsForRemoteUser: boolean = $ref(false);
|
||||||
let enableChartsForFederatedInstances: boolean = $ref(false);
|
let enableChartsForFederatedInstances: boolean = $ref(false);
|
||||||
|
@ -69,6 +77,7 @@ async function init() {
|
||||||
const meta = await os.api('admin/meta');
|
const meta = await os.api('admin/meta');
|
||||||
enableServerMachineStats = meta.enableServerMachineStats;
|
enableServerMachineStats = meta.enableServerMachineStats;
|
||||||
enableAchievements = meta.enableAchievements;
|
enableAchievements = meta.enableAchievements;
|
||||||
|
enableBotTrending = meta.enableBotTrending;
|
||||||
enableIdenticonGeneration = meta.enableIdenticonGeneration;
|
enableIdenticonGeneration = meta.enableIdenticonGeneration;
|
||||||
enableChartsForRemoteUser = meta.enableChartsForRemoteUser;
|
enableChartsForRemoteUser = meta.enableChartsForRemoteUser;
|
||||||
enableChartsForFederatedInstances = meta.enableChartsForFederatedInstances;
|
enableChartsForFederatedInstances = meta.enableChartsForFederatedInstances;
|
||||||
|
@ -78,6 +87,7 @@ function save() {
|
||||||
os.apiWithDialog('admin/update-meta', {
|
os.apiWithDialog('admin/update-meta', {
|
||||||
enableServerMachineStats,
|
enableServerMachineStats,
|
||||||
enableAchievements,
|
enableAchievements,
|
||||||
|
enableBotTrending,
|
||||||
enableIdenticonGeneration,
|
enableIdenticonGeneration,
|
||||||
enableChartsForRemoteUser,
|
enableChartsForRemoteUser,
|
||||||
enableChartsForFederatedInstances,
|
enableChartsForFederatedInstances,
|
||||||
|
|
|
@ -152,6 +152,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSwitch v-model="enableInfiniteScroll">{{ i18n.ts.enableInfiniteScroll }}</MkSwitch>
|
<MkSwitch v-model="enableInfiniteScroll">{{ i18n.ts.enableInfiniteScroll }}</MkSwitch>
|
||||||
<MkSwitch v-model="keepScreenOn">{{ i18n.ts.keepScreenOn }}</MkSwitch>
|
<MkSwitch v-model="keepScreenOn">{{ i18n.ts.keepScreenOn }}</MkSwitch>
|
||||||
<MkSwitch v-model="clickToOpen">{{ i18n.ts.clickToOpen }}</MkSwitch>
|
<MkSwitch v-model="clickToOpen">{{ i18n.ts.clickToOpen }}</MkSwitch>
|
||||||
|
<MkSwitch v-model="showBots">{{ i18n.ts.showBots }}</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
<MkSelect v-model="serverDisconnectedBehavior">
|
<MkSelect v-model="serverDisconnectedBehavior">
|
||||||
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
|
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
|
||||||
|
@ -227,6 +228,7 @@ const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showC
|
||||||
const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize'));
|
const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize'));
|
||||||
const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
|
const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
|
||||||
const clickToOpen = computed(defaultStore.makeGetterSetter('clickToOpen'));
|
const clickToOpen = computed(defaultStore.makeGetterSetter('clickToOpen'));
|
||||||
|
const showBots = computed(defaultStore.makeGetterSetter('tlWithBots'));
|
||||||
const collapseFiles = computed(defaultStore.makeGetterSetter('collapseFiles'));
|
const collapseFiles = computed(defaultStore.makeGetterSetter('collapseFiles'));
|
||||||
const autoloadConversation = computed(defaultStore.makeGetterSetter('autoloadConversation'));
|
const autoloadConversation = computed(defaultStore.makeGetterSetter('autoloadConversation'));
|
||||||
const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
|
const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
|
||||||
|
|
|
@ -21,6 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:withRenotes="withRenotes"
|
:withRenotes="withRenotes"
|
||||||
:withReplies="withReplies"
|
:withReplies="withReplies"
|
||||||
:onlyFiles="onlyFiles"
|
:onlyFiles="onlyFiles"
|
||||||
|
:withBots="withBots"
|
||||||
:sound="true"
|
:sound="true"
|
||||||
@queue="queueUpdated"
|
@queue="queueUpdated"
|
||||||
/>
|
/>
|
||||||
|
@ -63,6 +64,7 @@ let srcWhenNotSignin = $ref(isLocalTimelineAvailable ? 'local' : 'global');
|
||||||
const src = $computed({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin), set: (x) => saveSrc(x) });
|
const src = $computed({ get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin), set: (x) => saveSrc(x) });
|
||||||
const withRenotes = $ref(true);
|
const withRenotes = $ref(true);
|
||||||
const withReplies = $ref($i ? defaultStore.state.tlWithReplies : false);
|
const withReplies = $ref($i ? defaultStore.state.tlWithReplies : false);
|
||||||
|
const withBots = $ref($i ? defaultStore.state.tlWithBots : true);
|
||||||
const onlyFiles = $ref(false);
|
const onlyFiles = $ref(false);
|
||||||
|
|
||||||
watch($$(src), () => queue = 0);
|
watch($$(src), () => queue = 0);
|
||||||
|
|
|
@ -7,9 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSpacer :contentMax="narrow ? 800 : 1100" :style="background">
|
<MkSpacer :contentMax="narrow ? 800 : 1100" :style="background">
|
||||||
<div ref="rootEl" class="ftskorzw" :class="{ wide: !narrow }" style="container-type: inline-size;">
|
<div ref="rootEl" class="ftskorzw" :class="{ wide: !narrow }" style="container-type: inline-size;">
|
||||||
<div class="main _gaps">
|
<div class="main _gaps">
|
||||||
<!-- TODO -->
|
<MkInfo v-if="user.isSuspended" :warn="true">{{ i18n.ts.userSuspended }}</MkInfo>
|
||||||
<!-- <div class="punished" v-if="user.isSuspended"><i class="ph-warning ph-bold ph-lg" style="margin-right: 8px;"></i> {{ i18n.ts.userSuspended }}</div> -->
|
<MkInfo v-if="user.isSilenced" :warn="true">{{ i18n.ts.userSilenced }}</MkInfo>
|
||||||
<!-- <div class="punished" v-if="user.isSilenced"><i class="ph-warning ph-bold ph-lg" style="margin-right: 8px;"></i> {{ i18n.ts.userSilenced }}</div> -->
|
|
||||||
|
|
||||||
<div class="profile _gaps">
|
<div class="profile _gaps">
|
||||||
<MkAccountMoved v-if="user.movedTo" :movedTo="user.movedTo"/>
|
<MkAccountMoved v-if="user.movedTo" :movedTo="user.movedTo"/>
|
||||||
|
|
|
@ -373,6 +373,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
tlWithBots: {
|
||||||
|
where: 'device',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// TODO: 他のタブと永続化されたstateを同期
|
// TODO: 他のタブと永続化されたstateを同期
|
||||||
|
|
|
@ -31,6 +31,8 @@ export type Endpoints = {
|
||||||
'admin/show-users': { req: TODO; res: TODO; };
|
'admin/show-users': { req: TODO; res: TODO; };
|
||||||
'admin/silence-user': { req: TODO; res: TODO; };
|
'admin/silence-user': { req: TODO; res: TODO; };
|
||||||
'admin/suspend-user': { req: TODO; res: TODO; };
|
'admin/suspend-user': { req: TODO; res: TODO; };
|
||||||
|
'admin/nsfw-user': { req: TODO; res: TODO; };
|
||||||
|
'admin/unnsfw-user': { req: TODO; res: TODO; };
|
||||||
'admin/unsilence-user': { req: TODO; res: TODO; };
|
'admin/unsilence-user': { req: TODO; res: TODO; };
|
||||||
'admin/unsuspend-user': { req: TODO; res: TODO; };
|
'admin/unsuspend-user': { req: TODO; res: TODO; };
|
||||||
'admin/update-meta': { req: TODO; res: TODO; };
|
'admin/update-meta': { req: TODO; res: TODO; };
|
||||||
|
|
|
@ -405,6 +405,7 @@ export type AdminInstanceMetadata = DetailedInstanceMetadata & {
|
||||||
app192IconUrl: string | null;
|
app192IconUrl: string | null;
|
||||||
app512IconUrl: string | null;
|
app512IconUrl: string | null;
|
||||||
manifestJsonOverride: string;
|
manifestJsonOverride: string;
|
||||||
|
enableBotTrending: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ServerInfo = {
|
export type ServerInfo = {
|
||||||
|
|
Loading…
Reference in a new issue