This commit is contained in:
おさむのひと 2025-02-28 19:59:58 +09:00
parent 7a8ab424d4
commit 08b8bf94ef
8 changed files with 335 additions and 203 deletions

View File

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class AntennaRegexSupport1740781670204 {
name = 'AntennaRegexSupport1740781670204'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "antenna" ADD "useRegex" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "useRegex"`);
}
}

View File

@ -6,7 +6,8 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import RE2 from 're2'; import RE2 from 're2';
import { MiAntenna } from '@/models/Antenna.js'; import { IdService } from '@/core/IdService.js';
import { AntennaSource, MiAntenna } from '@/models/Antenna.js';
import type { MiNote } from '@/models/Note.js'; import type { MiNote } from '@/models/Note.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
@ -18,6 +19,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { RoleService } from './RoleService.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
type AntennaFilter = { type AntennaFilter = {
@ -81,7 +83,7 @@ function alwaysTrue(): boolean {
return true; return true;
} }
function createAntennaFilter(antenna: MiAntenna): AntennaFilter { export function createAntennaFilter(antenna: MiAntenna): AntennaFilter {
function createTestKeywordsFunction(antenna: MiAntenna): AntennaFilter['testKeywords'] { function createTestKeywordsFunction(antenna: MiAntenna): AntennaFilter['testKeywords'] {
// Clean up // Clean up
const keywords = antenna.keywords const keywords = antenna.keywords
@ -97,13 +99,12 @@ function createAntennaFilter(antenna: MiAntenna): AntennaFilter {
const keywordsPatterns = keywords.map(line => new RE2(line[0])); const keywordsPatterns = keywords.map(line => new RE2(line[0]));
const excludeKeywordsPatterns = excludeKeywords.map(line => new RE2(line[0])); const excludeKeywordsPatterns = excludeKeywords.map(line => new RE2(line[0]));
const regex = antennaFilters.regex;
if (keywords.length > 0 && excludeKeywords.length > 0) { if (keywords.length > 0 && excludeKeywords.length > 0) {
return (target: string) => regex.includeAndExclude(target, keywordsPatterns, excludeKeywordsPatterns); return (target: string) => antennaFilters.regex.includeAndExclude(target, keywordsPatterns, excludeKeywordsPatterns);
} else if (keywords.length > 0) { } else if (keywords.length > 0) {
return (target: string) => regex.includeOnly(target, keywordsPatterns); return (target: string) => antennaFilters.regex.includeOnly(target, keywordsPatterns);
} else if (excludeKeywords.length > 0) { } else if (excludeKeywords.length > 0) {
return (target: string) => regex.excludeOnly(target, excludeKeywordsPatterns); return (target: string) => antennaFilters.regex.excludeOnly(target, excludeKeywordsPatterns);
} else { } else {
return alwaysTrue; return alwaysTrue;
} }
@ -139,6 +140,19 @@ function createAntennaFilter(antenna: MiAntenna): AntennaFilter {
@Injectable() @Injectable()
export class AntennaService implements OnApplicationShutdown { export class AntennaService implements OnApplicationShutdown {
public static AntennaNotFoundError = class extends Error {
};
public static EmptyKeyWordError = class extends Error {
};
public static TooManyAntennasError = class extends Error {
};
public static InvalidRegexPatternError = class extends Error {
constructor(err?: unknown) {
const msg = err instanceof Error ? err.message : undefined;
super(msg);
}
};
private filtersFetched: boolean; private filtersFetched: boolean;
private filters: AntennaFilter[]; private filters: AntennaFilter[];
@ -152,9 +166,11 @@ export class AntennaService implements OnApplicationShutdown {
@Inject(DI.antennasRepository) @Inject(DI.antennasRepository)
private antennasRepository: AntennasRepository, private antennasRepository: AntennasRepository,
private roleService: RoleService,
private utilityService: UtilityService, private utilityService: UtilityService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private fanoutTimelineService: FanoutTimelineService, private fanoutTimelineService: FanoutTimelineService,
private idService: IdService,
) { ) {
this.filtersFetched = false; this.filtersFetched = false;
this.filters = []; this.filters = [];
@ -196,13 +212,10 @@ export class AntennaService implements OnApplicationShutdown {
@bindThis @bindThis
public async addNoteToAntennas(note: MiNote, noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; }): Promise<void> { public async addNoteToAntennas(note: MiNote, noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; }): Promise<void> {
const filters = await this.getFilters(); const filters = await this.getFilters();
const checkResults = await Promise.all(
filters.map(filter => this.checkHitAntenna(filter, note, noteUser).then(hit => [filter, hit] as const)),
);
const redisPipeline = this.redisForTimelines.pipeline(); const redisPipeline = this.redisForTimelines.pipeline();
for (const [filter, hit] of checkResults) {
if (hit) { for (const filter of filters) {
if (this.checkHitAntenna(filter, note, noteUser)) {
this.fanoutTimelineService.push(`antennaTimeline:${filter.antennaId}`, note.id, 200, redisPipeline); this.fanoutTimelineService.push(`antennaTimeline:${filter.antennaId}`, note.id, 200, redisPipeline);
this.globalEventService.publishAntennaStream(filter.antennaId, 'note', note); this.globalEventService.publishAntennaStream(filter.antennaId, 'note', note);
} }
@ -211,59 +224,158 @@ export class AntennaService implements OnApplicationShutdown {
redisPipeline.exec(); redisPipeline.exec();
} }
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
@bindThis @bindThis
private async checkHitAntenna( public async create(
filter: AntennaFilter, ps: {
note: (MiNote | Packed<'Note'>), name: string;
noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; }, src: AntennaSource;
): Promise<boolean> { keywords: string[][];
if (note.visibility === 'specified') return false; excludeKeywords: string[][];
if (note.visibility === 'followers') return false; users: string[];
caseSensitive: boolean;
localOnly?: boolean;
excludeBots?: boolean;
useRegex?: boolean;
withReplies: boolean;
withFile: boolean;
},
me: MiUser,
): Promise<MiAntenna> {
this.validateEmptyKeyWord(ps);
this.validateRegexPattern(ps);
const antenna = filter.src; const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id });
if (antenna.excludeBots && noteUser.isBot) return false; if (currentAntennasCount >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) {
if (antenna.localOnly && noteUser.host != null) return false; throw new AntennaService.TooManyAntennasError;
if (!antenna.withReplies && note.replyId != null) return false;
if (antenna.src === 'home') {
// TODO
} else if (antenna.src === 'list') {
// フロントエンドは塞がれているのでコメントアウト
// if (antenna.userListId == null) return false;
// const exists = await this.userListMembershipsRepository.exists({
// where: {
// userListId: antenna.userListId,
// userId: note.userId,
// },
// });
// if (!exists) return false;
} else if (antenna.src === 'users') {
if (!this.isIncludeUser(antenna, noteUser)) return false;
} else if (antenna.src === 'users_blacklist') {
if (this.isIncludeUser(antenna, noteUser)) return false;
} }
if (antenna.withFile) { const now = new Date();
if (note.fileIds && note.fileIds.length === 0) return false; const antenna = await this.antennasRepository.insertOne({
} id: this.idService.gen(now.getTime()),
lastUsedAt: now,
userId: me.id,
name: ps.name,
src: ps.src,
keywords: ps.keywords,
excludeKeywords: ps.excludeKeywords,
users: ps.users,
caseSensitive: ps.caseSensitive,
localOnly: ps.localOnly,
excludeBots: ps.excludeBots,
withReplies: ps.withReplies,
withFile: ps.withFile,
});
const _text = (note.text ?? '') + '\n' + (note.cw ?? ''); this.globalEventService.publishInternalEvent('antennaCreated', antenna);
return filter.testKeywords(_text);
return antenna;
} }
@bindThis @bindThis
private async getFilters() { public async update(
if (!this.filtersFetched) { ps: {
const antennas = await this.antennasRepository.findBy({ antennaId: string;
isActive: true, name?: string;
}); src?: AntennaSource;
this.filters = antennas.map(createAntennaFilter); keywords?: string[][];
this.filtersFetched = true; excludeKeywords?: string[][];
users?: string[];
caseSensitive?: boolean;
localOnly?: boolean;
excludeBots?: boolean;
useRegex?: boolean;
withReplies?: boolean;
withFile?: boolean;
},
me: MiUser,
): Promise<MiAntenna> {
this.validateEmptyKeyWord({
keywords: ps.keywords ?? [],
excludeKeywords: ps.excludeKeywords ?? [],
});
this.validateRegexPattern({
keywords: ps.keywords ?? [],
excludeKeywords: ps.excludeKeywords ?? [],
useRegex: ps.useRegex,
});
const antenna = await this.antennasRepository.findOneBy({
id: ps.antennaId,
userId: me.id,
});
if (antenna == null) {
throw new AntennaService.AntennaNotFoundError;
} }
return this.filters; await this.antennasRepository.update(antenna.id, {
name: ps.name,
src: ps.src,
keywords: ps.keywords,
excludeKeywords: ps.excludeKeywords,
users: ps.users,
caseSensitive: ps.caseSensitive,
localOnly: ps.localOnly,
excludeBots: ps.excludeBots,
withReplies: ps.withReplies,
withFile: ps.withFile,
isActive: true,
lastUsedAt: new Date(),
});
this.globalEventService.publishInternalEvent('antennaUpdated', await this.antennasRepository.findOneByOrFail({ id: antenna.id }));
return antenna;
}
@bindThis
private validateEmptyKeyWord(
ps: {
keywords: string[][];
excludeKeywords: string[][];
},
) {
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
throw new AntennaService.EmptyKeyWordError;
}
}
@bindThis
private validateRegexPattern(
ps: {
keywords: string[][];
excludeKeywords: string[][];
useRegex?: boolean;
},
) {
if (!ps.useRegex) {
return;
}
// NOTE: 正規表現パターンの場合は2次元配列の2次元目0番地にパターンが格納されていることを前提にする
if (ps.keywords.length > 0 && ps.keywords.some(x => x.length !== 1)) {
throw new AntennaService.InvalidRegexPatternError();
}
if (ps.excludeKeywords.length > 0 && ps.excludeKeywords.some(x => x.length !== 1)) {
throw new AntennaService.InvalidRegexPatternError();
}
for (const keywords of [ps.keywords, ps.excludeKeywords]) {
for (const keyword of keywords) {
const regexp = keyword[0].match(/^\/(.+)\/(.*)$/);
if (!regexp) {
throw new AntennaService.InvalidRegexPatternError();
}
try {
new RE2(regexp[1], regexp[2]);
} catch (err) {
throw new AntennaService.InvalidRegexPatternError(err);
}
}
}
} }
@bindThis @bindThis
@ -277,6 +389,44 @@ export class AntennaService implements OnApplicationShutdown {
return antennaUserAccounts.includes(noteUserAccount); return antennaUserAccounts.includes(noteUserAccount);
} }
@bindThis
private checkHitAntenna(
filter: AntennaFilter,
note: (MiNote | Packed<'Note'>),
noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; },
): boolean {
if (note.visibility === 'specified') return false;
if (note.visibility === 'followers') return false;
const antenna = filter.src;
if (antenna.excludeBots && noteUser.isBot) return false;
if (antenna.localOnly && noteUser.host != null) return false;
if (!antenna.withReplies && note.replyId != null) return false;
if (antenna.withFile) {
if (note.fileIds && note.fileIds.length === 0) return false;
}
if (antenna.src === 'users') {
if (!this.isIncludeUser(antenna, noteUser)) return false;
} else if (antenna.src === 'users_blacklist') {
if (this.isIncludeUser(antenna, noteUser)) return false;
}
const _text = (note.text ?? '') + '\n' + (note.cw ?? '');
return filter.testKeywords(_text);
}
@bindThis
private async getFilters() {
if (!this.filtersFetched) {
const antennas = await this.antennasRepository.findBy({ isActive: true });
this.filters = antennas.map(createAntennaFilter);
this.filtersFetched = true;
}
return this.filters;
}
@bindThis @bindThis
public dispose(): void { public dispose(): void {
this.redisForSub.off('message', this.onRedisMessage); this.redisForSub.off('message', this.onRedisMessage);

View File

@ -39,6 +39,7 @@ export class AntennaEntityService {
caseSensitive: antenna.caseSensitive, caseSensitive: antenna.caseSensitive,
localOnly: antenna.localOnly, localOnly: antenna.localOnly,
excludeBots: antenna.excludeBots, excludeBots: antenna.excludeBots,
useRegex: antenna.useRegex,
withReplies: antenna.withReplies, withReplies: antenna.withReplies,
withFile: antenna.withFile, withFile: antenna.withFile,
isActive: antenna.isActive, isActive: antenna.isActive,

View File

@ -9,6 +9,15 @@ import { MiUser } from './User.js';
import { MiUserList } from './UserList.js'; import { MiUserList } from './UserList.js';
import { id } from './util/id.js'; import { id } from './util/id.js';
export const antennaSources = [
'all',
'users',
'users_blacklist',
// 'home', // TODO
// 'list', // NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
] as const;
export type AntennaSource = typeof antennaSources[number];
@Entity('antenna') @Entity('antenna')
export class MiAntenna { export class MiAntenna {
@PrimaryColumn(id()) @PrimaryColumn(id())
@ -37,8 +46,8 @@ export class MiAntenna {
}) })
public name: string; public name: string;
@Column('enum', { enum: ['home', 'all', 'users', 'list', 'users_blacklist'] }) @Column('enum', { enum: antennaSources })
public src: 'home' | 'all' | 'users' | 'list' | 'users_blacklist'; public src: AntennaSource;
@Column({ @Column({
...id(), ...id(),

View File

@ -72,6 +72,10 @@ export const packedAntennaSchema = {
optional: false, nullable: false, optional: false, nullable: false,
default: false, default: false,
}, },
useRegex: {
type: 'boolean',
optional: false, nullable: false,
},
excludeBots: { excludeBots: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,

View File

@ -3,14 +3,11 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { AntennaService } from '@/core/AntennaService.js';
import { IdService } from '@/core/IdService.js';
import type { UserListsRepository, AntennasRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js'; import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js';
import { DI } from '@/di-symbols.js'; import { antennaSources } from '@/models/Antenna.js';
import { RoleService } from '@/core/RoleService.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@ -40,6 +37,13 @@ export const meta = {
code: 'EMPTY_KEYWORD', code: 'EMPTY_KEYWORD',
id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a', id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a',
}, },
invalidRegexPattern: {
message: 'Invalid regex pattern.',
code: 'INVALID_REGEX_PATTERN',
id: 'b06d08f4-6434-5faa-0fdd-a2aaf85e9de7',
httpStatusCode: 400,
},
}, },
res: { res: {
@ -53,91 +57,65 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
name: { type: 'string', minLength: 1, maxLength: 100 }, name: { type: 'string', minLength: 1, maxLength: 100 },
src: { type: 'string', enum: ['home', 'all', 'users', 'list', 'users_blacklist'] }, src: { type: 'string', enum: antennaSources },
userListId: { type: 'string', format: 'misskey:id', nullable: true }, keywords: {
keywords: { type: 'array', items: { type: 'array',
type: 'array', items: { items: {
type: 'string', type: 'array',
items: { type: 'string' },
}, },
} }, },
excludeKeywords: { type: 'array', items: { excludeKeywords: {
type: 'array', items: { type: 'array',
type: 'string', items: {
type: 'array',
items: { type: 'string' },
}, },
} }, },
users: { type: 'array', items: { users: {
type: 'string', type: 'array',
} }, items: { type: 'string' },
},
caseSensitive: { type: 'boolean' }, caseSensitive: { type: 'boolean' },
localOnly: { type: 'boolean' }, localOnly: { type: 'boolean' },
excludeBots: { type: 'boolean' }, excludeBots: { type: 'boolean' },
useRegex: { type: 'boolean' },
withReplies: { type: 'boolean' }, withReplies: { type: 'boolean' },
withFile: { type: 'boolean' }, withFile: { type: 'boolean' },
}, },
required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile'], required: [
'name',
'src',
'keywords',
'excludeKeywords',
'users',
'caseSensitive',
'withReplies',
'withFile',
],
} as const; } as const;
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.antennasRepository) private readonly antennaEntityService: AntennaEntityService,
private antennasRepository: AntennasRepository, private readonly antennaService: AntennaService,
@Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository,
private antennaEntityService: AntennaEntityService,
private roleService: RoleService,
private idService: IdService,
private globalEventService: GlobalEventService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) { try {
throw new ApiError(meta.errors.emptyKeyword); const antenna = await this.antennaService.create(ps, me);
} return this.antennaEntityService.pack(antenna);
} catch (e) {
const currentAntennasCount = await this.antennasRepository.countBy({ if (e instanceof AntennaService.EmptyKeyWordError) {
userId: me.id, throw new ApiError(meta.errors.emptyKeyword);
}); } else if (e instanceof AntennaService.TooManyAntennasError) {
if (currentAntennasCount >= (await this.roleService.getUserPolicies(me.id)).antennaLimit) { throw new ApiError(meta.errors.tooManyAntennas);
throw new ApiError(meta.errors.tooManyAntennas); } else if (e instanceof AntennaService.InvalidRegexPatternError) {
} throw new ApiError(meta.errors.invalidRegexPattern);
} else {
let userList; throw e;
if (ps.src === 'list' && ps.userListId) {
userList = await this.userListsRepository.findOneBy({
id: ps.userListId,
userId: me.id,
});
if (userList == null) {
throw new ApiError(meta.errors.noSuchUserList);
} }
} }
const now = new Date();
const antenna = await this.antennasRepository.insertOne({
id: this.idService.gen(now.getTime()),
lastUsedAt: now,
userId: me.id,
name: ps.name,
src: ps.src,
userListId: userList ? userList.id : null,
keywords: ps.keywords,
excludeKeywords: ps.excludeKeywords,
users: ps.users,
caseSensitive: ps.caseSensitive,
localOnly: ps.localOnly,
excludeBots: ps.excludeBots,
withReplies: ps.withReplies,
withFile: ps.withFile,
});
this.globalEventService.publishInternalEvent('antennaCreated', antenna);
return await this.antennaEntityService.pack(antenna);
}); });
} }
} }

View File

@ -4,6 +4,8 @@
*/ */
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { AntennaService } from '@/core/AntennaService.js';
import { antennaSources } from '@/models/Antenna.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import type { AntennasRepository, UserListsRepository } from '@/models/_.js'; import type { AntennasRepository, UserListsRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
@ -38,6 +40,13 @@ export const meta = {
code: 'EMPTY_KEYWORD', code: 'EMPTY_KEYWORD',
id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4', id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4',
}, },
invalidRegexPattern: {
message: 'Invalid regex pattern.',
code: 'INVALID_REGEX_PATTERN',
id: 'dbb44ec3-5d15-508d-e6b2-71f3794c6a41',
httpStatusCode: 400,
},
}, },
res: { res: {
@ -52,24 +61,29 @@ export const paramDef = {
properties: { properties: {
antennaId: { type: 'string', format: 'misskey:id' }, antennaId: { type: 'string', format: 'misskey:id' },
name: { type: 'string', minLength: 1, maxLength: 100 }, name: { type: 'string', minLength: 1, maxLength: 100 },
src: { type: 'string', enum: ['home', 'all', 'users', 'list', 'users_blacklist'] }, src: { type: 'string', enum: antennaSources },
userListId: { type: 'string', format: 'misskey:id', nullable: true }, keywords: {
keywords: { type: 'array', items: { type: 'array',
type: 'array', items: { items: {
type: 'string', type: 'array',
items: { type: 'string' },
}, },
} }, },
excludeKeywords: { type: 'array', items: { excludeKeywords: {
type: 'array', items: { type: 'array',
type: 'string', items: {
type: 'array',
items: { type: 'string' },
}, },
} }, },
users: { type: 'array', items: { users: {
type: 'string', type: 'array',
} }, items: { type: 'string' },
},
caseSensitive: { type: 'boolean' }, caseSensitive: { type: 'boolean' },
localOnly: { type: 'boolean' }, localOnly: { type: 'boolean' },
excludeBots: { type: 'boolean' }, excludeBots: { type: 'boolean' },
useRegex: { type: 'boolean' },
withReplies: { type: 'boolean' }, withReplies: { type: 'boolean' },
withFile: { type: 'boolean' }, withFile: { type: 'boolean' },
}, },
@ -79,63 +93,24 @@ export const paramDef = {
@Injectable() @Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor( constructor(
@Inject(DI.antennasRepository) private readonly antennaEntityService: AntennaEntityService,
private antennasRepository: AntennasRepository, private readonly antennaService: AntennaService,
@Inject(DI.userListsRepository)
private userListsRepository: UserListsRepository,
private antennaEntityService: AntennaEntityService,
private globalEventService: GlobalEventService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
if (ps.keywords && ps.excludeKeywords) { try {
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) { const antenna = await this.antennaService.update(ps, me);
return this.antennaEntityService.pack(antenna);
} catch (e) {
if (e instanceof AntennaService.EmptyKeyWordError) {
throw new ApiError(meta.errors.emptyKeyword); throw new ApiError(meta.errors.emptyKeyword);
} else if (e instanceof AntennaService.AntennaNotFoundError) {
throw new ApiError(meta.errors.noSuchAntenna);
} else if (e instanceof AntennaService.InvalidRegexPatternError) {
throw new ApiError(meta.errors.invalidRegexPattern);
} else {
throw e;
} }
} }
// Fetch the antenna
const antenna = await this.antennasRepository.findOneBy({
id: ps.antennaId,
userId: me.id,
});
if (antenna == null) {
throw new ApiError(meta.errors.noSuchAntenna);
}
let userList;
if ((ps.src === 'list' || antenna.src === 'list') && ps.userListId) {
userList = await this.userListsRepository.findOneBy({
id: ps.userListId,
userId: me.id,
});
if (userList == null) {
throw new ApiError(meta.errors.noSuchUserList);
}
}
await this.antennasRepository.update(antenna.id, {
name: ps.name,
src: ps.src,
userListId: ps.userListId !== undefined ? userList ? userList.id : null : undefined,
keywords: ps.keywords,
excludeKeywords: ps.excludeKeywords,
users: ps.users,
caseSensitive: ps.caseSensitive,
localOnly: ps.localOnly,
excludeBots: ps.excludeBots,
withReplies: ps.withReplies,
withFile: ps.withFile,
isActive: true,
lastUsedAt: new Date(),
});
this.globalEventService.publishInternalEvent('antennaUpdated', await this.antennasRepository.findOneByOrFail({ id: antenna.id }));
return await this.antennaEntityService.pack(antenna.id);
}); });
} }
} }

View File

@ -4651,6 +4651,7 @@ export type components = {
caseSensitive: boolean; caseSensitive: boolean;
/** @default false */ /** @default false */
localOnly: boolean; localOnly: boolean;
useRegex: boolean;
/** @default false */ /** @default false */
excludeBots: boolean; excludeBots: boolean;
/** @default false */ /** @default false */
@ -10918,15 +10919,14 @@ export type operations = {
'application/json': { 'application/json': {
name: string; name: string;
/** @enum {string} */ /** @enum {string} */
src: 'home' | 'all' | 'users' | 'list' | 'users_blacklist'; src: 'all' | 'users' | 'users_blacklist';
/** Format: misskey:id */
userListId?: string | null;
keywords: string[][]; keywords: string[][];
excludeKeywords: string[][]; excludeKeywords: string[][];
users: string[]; users: string[];
caseSensitive: boolean; caseSensitive: boolean;
localOnly?: boolean; localOnly?: boolean;
excludeBots?: boolean; excludeBots?: boolean;
useRegex?: boolean;
withReplies: boolean; withReplies: boolean;
withFile: boolean; withFile: boolean;
}; };
@ -11199,15 +11199,14 @@ export type operations = {
antennaId: string; antennaId: string;
name?: string; name?: string;
/** @enum {string} */ /** @enum {string} */
src?: 'home' | 'all' | 'users' | 'list' | 'users_blacklist'; src?: 'all' | 'users' | 'users_blacklist';
/** Format: misskey:id */
userListId?: string | null;
keywords?: string[][]; keywords?: string[][];
excludeKeywords?: string[][]; excludeKeywords?: string[][];
users?: string[]; users?: string[];
caseSensitive?: boolean; caseSensitive?: boolean;
localOnly?: boolean; localOnly?: boolean;
excludeBots?: boolean; excludeBots?: boolean;
useRegex?: boolean;
withReplies?: boolean; withReplies?: boolean;
withFile?: boolean; withFile?: boolean;
}; };