2023-07-27 07:31:52 +02:00
|
|
|
/*
|
2024-02-13 16:59:27 +01:00
|
|
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
2023-07-27 07:31:52 +02:00
|
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
*/
|
|
|
|
|
2022-09-17 20:27:08 +02:00
|
|
|
import { Inject, Injectable } from '@nestjs/common';
|
2023-04-14 06:50:05 +02:00
|
|
|
import * as Redis from 'ioredis';
|
2023-09-20 04:33:36 +02:00
|
|
|
import type { MiAntenna } from '@/models/Antenna.js';
|
|
|
|
import type { MiNote } from '@/models/Note.js';
|
|
|
|
import type { MiUser } from '@/models/User.js';
|
2022-09-17 20:27:08 +02:00
|
|
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
|
|
|
import * as Acct from '@/misc/acct.js';
|
2023-03-10 06:22:37 +01:00
|
|
|
import type { Packed } from '@/misc/json-schema.js';
|
2022-09-17 20:27:08 +02:00
|
|
|
import { DI } from '@/di-symbols.js';
|
2023-10-03 13:26:11 +02:00
|
|
|
import type { AntennasRepository, UserListMembershipsRepository } from '@/models/_.js';
|
2022-12-04 02:16:03 +01:00
|
|
|
import { UtilityService } from '@/core/UtilityService.js';
|
2022-12-04 07:03:09 +01:00
|
|
|
import { bindThis } from '@/decorators.js';
|
2023-09-29 04:29:54 +02:00
|
|
|
import type { GlobalEvents } from '@/core/GlobalEventService.js';
|
2023-11-26 02:02:22 +01:00
|
|
|
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
2023-01-09 06:12:42 +01:00
|
|
|
import type { OnApplicationShutdown } from '@nestjs/common';
|
2022-09-17 20:27:08 +02:00
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
export class AntennaService implements OnApplicationShutdown {
|
2022-09-18 20:11:50 +02:00
|
|
|
private antennasFetched: boolean;
|
2023-08-16 10:51:28 +02:00
|
|
|
private antennas: MiAntenna[];
|
2022-09-17 20:27:08 +02:00
|
|
|
|
|
|
|
constructor(
|
2023-10-03 13:26:11 +02:00
|
|
|
@Inject(DI.redisForTimelines)
|
|
|
|
private redisForTimelines: Redis.Redis,
|
2023-04-03 05:11:16 +02:00
|
|
|
|
2023-04-09 10:09:27 +02:00
|
|
|
@Inject(DI.redisForSub)
|
|
|
|
private redisForSub: Redis.Redis,
|
2022-09-17 20:27:08 +02:00
|
|
|
|
|
|
|
@Inject(DI.antennasRepository)
|
|
|
|
private antennasRepository: AntennasRepository,
|
|
|
|
|
2023-10-03 13:26:11 +02:00
|
|
|
@Inject(DI.userListMembershipsRepository)
|
|
|
|
private userListMembershipsRepository: UserListMembershipsRepository,
|
2022-09-17 20:27:08 +02:00
|
|
|
|
|
|
|
private utilityService: UtilityService,
|
2023-02-04 02:02:03 +01:00
|
|
|
private globalEventService: GlobalEventService,
|
2023-11-26 02:02:22 +01:00
|
|
|
private fanoutTimelineService: FanoutTimelineService,
|
2022-09-17 20:27:08 +02:00
|
|
|
) {
|
2022-09-18 20:11:50 +02:00
|
|
|
this.antennasFetched = false;
|
|
|
|
this.antennas = [];
|
2022-09-17 20:27:08 +02:00
|
|
|
|
2023-04-09 10:09:27 +02:00
|
|
|
this.redisForSub.on('message', this.onRedisMessage);
|
2022-09-17 20:27:08 +02:00
|
|
|
}
|
|
|
|
|
2022-12-04 07:03:09 +01:00
|
|
|
@bindThis
|
2022-09-23 23:45:44 +02:00
|
|
|
private async onRedisMessage(_: string, data: string): Promise<void> {
|
2022-09-17 20:27:08 +02:00
|
|
|
const obj = JSON.parse(data);
|
|
|
|
|
|
|
|
if (obj.channel === 'internal') {
|
2023-09-29 04:29:54 +02:00
|
|
|
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
2022-09-17 20:27:08 +02:00
|
|
|
switch (type) {
|
|
|
|
case 'antennaCreated':
|
2024-01-22 09:44:03 +01:00
|
|
|
this.antennas.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
2023-01-25 03:18:16 +01:00
|
|
|
...body,
|
2023-03-20 12:12:38 +01:00
|
|
|
lastUsedAt: new Date(body.lastUsedAt),
|
2024-01-22 09:44:03 +01:00
|
|
|
user: null, // joinなカラムは通常取ってこないので
|
|
|
|
userList: null, // joinなカラムは通常取ってこないので
|
2023-01-25 03:18:16 +01:00
|
|
|
});
|
2022-09-17 20:27:08 +02:00
|
|
|
break;
|
2023-11-21 07:32:34 +01:00
|
|
|
case 'antennaUpdated': {
|
|
|
|
const idx = this.antennas.findIndex(a => a.id === body.id);
|
|
|
|
if (idx >= 0) {
|
2024-01-22 09:44:03 +01:00
|
|
|
this.antennas[idx] = { // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
2023-11-21 07:32:34 +01:00
|
|
|
...body,
|
|
|
|
lastUsedAt: new Date(body.lastUsedAt),
|
2024-01-22 09:44:03 +01:00
|
|
|
user: null, // joinなカラムは通常取ってこないので
|
|
|
|
userList: null, // joinなカラムは通常取ってこないので
|
2023-11-21 07:32:34 +01:00
|
|
|
};
|
|
|
|
} else {
|
|
|
|
// サーバ起動時にactiveじゃなかった場合、リストに持っていないので追加する必要あり
|
2024-01-22 09:44:03 +01:00
|
|
|
this.antennas.push({ // TODO: このあたりのデシリアライズ処理は各modelファイル内に関数としてexportしたい
|
2023-11-21 07:32:34 +01:00
|
|
|
...body,
|
|
|
|
lastUsedAt: new Date(body.lastUsedAt),
|
2024-01-22 09:44:03 +01:00
|
|
|
user: null, // joinなカラムは通常取ってこないので
|
|
|
|
userList: null, // joinなカラムは通常取ってこないので
|
2023-11-21 07:32:34 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2022-09-17 20:27:08 +02:00
|
|
|
break;
|
|
|
|
case 'antennaDeleted':
|
2022-09-18 20:11:50 +02:00
|
|
|
this.antennas = this.antennas.filter(a => a.id !== body.id);
|
2022-09-17 20:27:08 +02:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-04 07:03:09 +01:00
|
|
|
@bindThis
|
2024-03-20 23:51:01 +01:00
|
|
|
public async addNoteToAntennas(note: MiNote, noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; }): Promise<void> {
|
2023-04-12 03:07:14 +02:00
|
|
|
const antennas = await this.getAntennas();
|
|
|
|
const antennasWithMatchResult = await Promise.all(antennas.map(antenna => this.checkHitAntenna(antenna, note, noteUser).then(hit => [antenna, hit] as const)));
|
|
|
|
const matchedAntennas = antennasWithMatchResult.filter(([, hit]) => hit).map(([antenna]) => antenna);
|
|
|
|
|
2023-10-03 13:26:11 +02:00
|
|
|
const redisPipeline = this.redisForTimelines.pipeline();
|
2023-04-12 03:07:14 +02:00
|
|
|
|
|
|
|
for (const antenna of matchedAntennas) {
|
2023-11-26 02:02:22 +01:00
|
|
|
this.fanoutTimelineService.push(`antennaTimeline:${antenna.id}`, note.id, 200, redisPipeline);
|
2023-04-12 03:07:14 +02:00
|
|
|
this.globalEventService.publishAntennaStream(antenna.id, 'note', note);
|
|
|
|
}
|
|
|
|
|
|
|
|
redisPipeline.exec();
|
2022-09-17 20:27:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// NOTE: フォローしているユーザーのノート、リストのユーザーのノート、グループのユーザーのノート指定はパフォーマンス上の理由で無効になっている
|
|
|
|
|
2022-12-04 07:03:09 +01:00
|
|
|
@bindThis
|
2024-03-20 23:51:01 +01:00
|
|
|
public async checkHitAntenna(antenna: MiAntenna, note: (MiNote | Packed<'Note'>), noteUser: { id: MiUser['id']; username: string; host: string | null; isBot: boolean; }): Promise<boolean> {
|
2022-09-17 20:27:08 +02:00
|
|
|
if (note.visibility === 'specified') return false;
|
2023-01-09 06:12:42 +01:00
|
|
|
if (note.visibility === 'followers') return false;
|
2023-07-08 00:08:16 +02:00
|
|
|
|
2024-03-20 23:51:01 +01:00
|
|
|
if (antenna.excludeBots && noteUser.isBot) return false;
|
|
|
|
|
2023-10-16 08:06:00 +02:00
|
|
|
if (antenna.localOnly && noteUser.host != null) return false;
|
|
|
|
|
2022-09-17 20:27:08 +02:00
|
|
|
if (!antenna.withReplies && note.replyId != null) return false;
|
2023-07-08 00:08:16 +02:00
|
|
|
|
2022-09-17 20:27:08 +02:00
|
|
|
if (antenna.src === 'home') {
|
2023-01-09 06:12:42 +01:00
|
|
|
// TODO
|
2022-09-17 20:27:08 +02:00
|
|
|
} else if (antenna.src === 'list') {
|
2023-10-03 13:26:11 +02:00
|
|
|
const listUsers = (await this.userListMembershipsRepository.findBy({
|
2022-09-17 20:27:08 +02:00
|
|
|
userListId: antenna.userListId!,
|
|
|
|
})).map(x => x.userId);
|
2023-07-08 00:08:16 +02:00
|
|
|
|
2022-09-17 20:27:08 +02:00
|
|
|
if (!listUsers.includes(note.userId)) return false;
|
|
|
|
} else if (antenna.src === 'users') {
|
|
|
|
const accts = antenna.users.map(x => {
|
|
|
|
const { username, host } = Acct.parse(x);
|
|
|
|
return this.utilityService.getFullApAccount(username, host).toLowerCase();
|
|
|
|
});
|
|
|
|
if (!accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
|
2023-09-22 09:52:43 +02:00
|
|
|
} else if (antenna.src === 'users_blacklist') {
|
|
|
|
const accts = antenna.users.map(x => {
|
|
|
|
const { username, host } = Acct.parse(x);
|
|
|
|
return this.utilityService.getFullApAccount(username, host).toLowerCase();
|
|
|
|
});
|
|
|
|
if (accts.includes(this.utilityService.getFullApAccount(noteUser.username, noteUser.host).toLowerCase())) return false;
|
2022-09-17 20:27:08 +02:00
|
|
|
}
|
2023-07-08 00:08:16 +02:00
|
|
|
|
2022-09-17 20:27:08 +02:00
|
|
|
const keywords = antenna.keywords
|
|
|
|
// Clean up
|
|
|
|
.map(xs => xs.filter(x => x !== ''))
|
|
|
|
.filter(xs => xs.length > 0);
|
2023-07-08 00:08:16 +02:00
|
|
|
|
2022-09-17 20:27:08 +02:00
|
|
|
if (keywords.length > 0) {
|
2023-02-28 12:20:23 +01:00
|
|
|
if (note.text == null && note.cw == null) return false;
|
|
|
|
|
|
|
|
const _text = (note.text ?? '') + '\n' + (note.cw ?? '');
|
2023-07-08 00:08:16 +02:00
|
|
|
|
2022-09-17 20:27:08 +02:00
|
|
|
const matched = keywords.some(and =>
|
|
|
|
and.every(keyword =>
|
|
|
|
antenna.caseSensitive
|
2023-02-28 12:20:23 +01:00
|
|
|
? _text.includes(keyword)
|
|
|
|
: _text.toLowerCase().includes(keyword.toLowerCase()),
|
2022-09-17 20:27:08 +02:00
|
|
|
));
|
2023-07-08 00:08:16 +02:00
|
|
|
|
2022-09-17 20:27:08 +02:00
|
|
|
if (!matched) return false;
|
|
|
|
}
|
2023-07-08 00:08:16 +02:00
|
|
|
|
2022-09-17 20:27:08 +02:00
|
|
|
const excludeKeywords = antenna.excludeKeywords
|
|
|
|
// Clean up
|
|
|
|
.map(xs => xs.filter(x => x !== ''))
|
|
|
|
.filter(xs => xs.length > 0);
|
2023-07-08 00:08:16 +02:00
|
|
|
|
2022-09-17 20:27:08 +02:00
|
|
|
if (excludeKeywords.length > 0) {
|
2023-02-28 12:20:23 +01:00
|
|
|
if (note.text == null && note.cw == null) return false;
|
|
|
|
|
|
|
|
const _text = (note.text ?? '') + '\n' + (note.cw ?? '');
|
|
|
|
|
2022-09-17 20:27:08 +02:00
|
|
|
const matched = excludeKeywords.some(and =>
|
|
|
|
and.every(keyword =>
|
|
|
|
antenna.caseSensitive
|
2023-02-28 12:20:23 +01:00
|
|
|
? _text.includes(keyword)
|
|
|
|
: _text.toLowerCase().includes(keyword.toLowerCase()),
|
2022-09-17 20:27:08 +02:00
|
|
|
));
|
2023-07-08 00:08:16 +02:00
|
|
|
|
2022-09-17 20:27:08 +02:00
|
|
|
if (matched) return false;
|
|
|
|
}
|
2023-07-08 00:08:16 +02:00
|
|
|
|
2022-09-17 20:27:08 +02:00
|
|
|
if (antenna.withFile) {
|
|
|
|
if (note.fileIds && note.fileIds.length === 0) return false;
|
|
|
|
}
|
2023-07-08 00:08:16 +02:00
|
|
|
|
2022-09-17 20:27:08 +02:00
|
|
|
// TODO: eval expression
|
2023-07-08 00:08:16 +02:00
|
|
|
|
2022-09-17 20:27:08 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-12-04 07:03:09 +01:00
|
|
|
@bindThis
|
2022-09-17 20:27:08 +02:00
|
|
|
public async getAntennas() {
|
2022-09-18 20:11:50 +02:00
|
|
|
if (!this.antennasFetched) {
|
2023-03-20 12:12:38 +01:00
|
|
|
this.antennas = await this.antennasRepository.findBy({
|
|
|
|
isActive: true,
|
|
|
|
});
|
2022-09-18 20:11:50 +02:00
|
|
|
this.antennasFetched = true;
|
2022-09-17 20:27:08 +02:00
|
|
|
}
|
2023-07-08 00:08:16 +02:00
|
|
|
|
2022-09-18 20:11:50 +02:00
|
|
|
return this.antennas;
|
2022-09-17 20:27:08 +02:00
|
|
|
}
|
2023-05-29 06:21:26 +02:00
|
|
|
|
|
|
|
@bindThis
|
|
|
|
public dispose(): void {
|
|
|
|
this.redisForSub.off('message', this.onRedisMessage);
|
|
|
|
}
|
|
|
|
|
|
|
|
@bindThis
|
|
|
|
public onApplicationShutdown(signal?: string | undefined): void {
|
|
|
|
this.dispose();
|
|
|
|
}
|
2022-09-17 20:27:08 +02:00
|
|
|
}
|