1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
|
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { bindThis } from '@/decorators.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { isRenotePacked, isQuotePacked, isPackedPureRenote } from '@/misc/is-renote.js';
import type { Packed } from '@/misc/json-schema.js';
import type { JsonObject, JsonValue } from '@/misc/json-value.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import type Connection from './Connection.js';
/**
* Stream channel
*/
// eslint-disable-next-line import/no-default-export
export default abstract class Channel {
protected readonly noteEntityService: NoteEntityService;
protected connection: Connection;
public id: string;
public abstract readonly chName: string;
public static readonly shouldShare: boolean;
public static readonly requireCredential: boolean;
public static readonly kind?: string | null;
protected get user() {
return this.connection.user;
}
protected get userProfile() {
return this.connection.userProfile;
}
protected get following() {
return this.connection.following;
}
protected get userIdsWhoMeMuting() {
return this.connection.userIdsWhoMeMuting;
}
protected get userIdsWhoMeMutingRenotes() {
return this.connection.userIdsWhoMeMutingRenotes;
}
protected get userIdsWhoBlockingMe() {
return this.connection.userIdsWhoBlockingMe;
}
protected get userMutedInstances() {
return this.connection.userMutedInstances;
}
protected get followingChannels() {
return this.connection.followingChannels;
}
protected get subscriber() {
return this.connection.subscriber;
}
/*
* ミュートとブロックされてるを処理する
*/
protected isNoteMutedOrBlocked(note: Packed<'Note'>): boolean {
// 流れてきたNoteがインスタンスミュートしたインスタンスが関わる
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? [])) && !this.following[note.userId]) return true;
// 流れてきたNoteがミュートしているユーザーが関わる
if (isUserRelated(note, this.userIdsWhoMeMuting)) return true;
// 流れてきたNoteがブロックされているユーザーが関わる
if (isUserRelated(note, this.userIdsWhoBlockingMe)) return true;
// 流れてきたNoteがリノートをミュートしてるユーザが行ったもの
if (isRenotePacked(note) && !isQuotePacked(note) && this.userIdsWhoMeMutingRenotes.has(note.user.id)) return true;
// If it's a boost (pure renote) then we need to check the target as well
if (isPackedPureRenote(note) && note.renote && this.isNoteMutedOrBlocked(note.renote)) return true;
return false;
}
/**
* This function modifies {@link note}, please make sure it has been shallow cloned.
* See Dakkar's comment of {@link assignMyReaction} for more
* @param note The note to change
*/
protected async hideNote(note: Packed<'Note'>): Promise<void> {
if (note.renote) {
await this.hideNote(note.renote);
}
if (note.reply) {
await this.hideNote(note.reply);
}
const meId = this.user?.id ?? null;
await this.noteEntityService.hideNote(note, meId);
}
constructor(id: string, connection: Connection, noteEntityService: NoteEntityService) {
this.id = id;
this.connection = connection;
this.noteEntityService = noteEntityService;
}
public send(payload: { type: string, body: JsonValue }): void
public send(type: string, payload: JsonValue): void
@bindThis
public send(typeOrPayload: { type: string, body: JsonValue } | string, payload?: JsonValue) {
const type = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).type : (typeOrPayload as string);
const body = payload === undefined ? (typeOrPayload as { type: string, body: JsonValue }).body : payload;
this.connection.sendMessageToWs('channel', {
id: this.id,
type: type,
body: body,
});
}
public abstract init(params: JsonObject): void;
public dispose?(): void;
public onMessage?(type: string, body: JsonValue): void;
public async assignMyReaction(note: Packed<'Note'>): Promise<Packed<'Note'>> {
// StreamingApiServerService creates a single EventEmitter per server process,
// so a new note arriving from redis gets de-serialised once per server process,
// and then that single object is passed to all active channels on each connection.
// If we didn't clone the notes here, different connections would asynchronously write
// different values to the same object, resulting in a random value being sent to each frontend. -- Dakkar
const clonedNote = { ...note };
if (this.user && isRenotePacked(note) && !isQuotePacked(note)) {
if (note.renote && Object.keys(note.renote.reactions).length > 0) {
const myReaction = await this.noteEntityService.populateMyReaction(note.renote, this.user.id);
if (myReaction) {
clonedNote.renote = { ...note.renote };
clonedNote.renote.myReaction = myReaction;
}
}
if (note.renote?.reply && Object.keys(note.renote.reply.reactions).length > 0) {
const myReaction = await this.noteEntityService.populateMyReaction(note.renote.reply, this.user.id);
if (myReaction) {
clonedNote.renote = { ...note.renote };
clonedNote.renote.reply = { ...note.renote.reply };
clonedNote.renote.reply.myReaction = myReaction;
}
}
}
if (this.user && note.reply && Object.keys(note.reply.reactions).length > 0) {
const myReaction = await this.noteEntityService.populateMyReaction(note.reply, this.user.id);
if (myReaction) {
clonedNote.reply = { ...note.reply };
clonedNote.reply.myReaction = myReaction;
}
}
return clonedNote;
}
}
export type MiChannelService<T extends boolean> = {
shouldShare: boolean;
requireCredential: T;
kind: T extends true ? string : string | null | undefined;
create: (id: string, connection: Connection) => Channel;
}
|