Merge branch 'stream-types' into swn

This commit is contained in:
tamaina
2021-09-17 22:50:41 +09:00
44 changed files with 695 additions and 186 deletions

View File

@@ -93,7 +93,7 @@ export async function readGroupMessagingMessage(
id: In(messageIds)
});
const reads = [];
const reads: MessagingMessage['id'][] = [];
for (const message of messages) {
if (message.userId === userId) continue;

View File

@@ -137,7 +137,7 @@ export default define(meta, async (ps, user) => {
notify: ps.notify,
});
publishInternalEvent('antennaUpdated', Antennas.findOneOrFail(antenna.id));
publishInternalEvent('antennaUpdated', await Antennas.findOneOrFail(antenna.id));
return await Antennas.pack(antenna.id);
});

View File

@@ -3,6 +3,7 @@ import Channel from '../channel';
import { Notes } from '@/models/index';
import { isMutedUserRelated } from '@/misc/is-muted-user-related';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
import { StreamMessages } from '../types';
export default class extends Channel {
public readonly chName = 'antenna';
@@ -19,11 +20,9 @@ export default class extends Channel {
}
@autobind
private async onEvent(data: any) {
const { type, body } = data;
if (type === 'note') {
const note = await Notes.pack(body.id, this.user, { detail: true });
private async onEvent(data: StreamMessages['antenna']['spec']) {
if (data.type === 'note') {
const note = await Notes.pack(data.body.id, this.user, { detail: true });
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isMutedUserRelated(note, this.muting)) return;
@@ -33,8 +32,6 @@ export default class extends Channel {
this.connection.cacheNote(note);
this.send('note', note);
} else {
this.send(type, body);
}
}

View File

@@ -3,8 +3,9 @@ import Channel from '../channel';
import { Notes, Users } from '@/models/index';
import { isMutedUserRelated } from '@/misc/is-muted-user-related';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
import { PackedNote } from '@/models/repositories/note';
import { User } from '@/models/entities/user';
import { StreamMessages } from '../types';
import { Packed } from '@/misc/schema';
export default class extends Channel {
public readonly chName = 'channel';
@@ -25,7 +26,7 @@ export default class extends Channel {
}
@autobind
private async onNote(note: PackedNote) {
private async onNote(note: Packed<'Note'>) {
if (note.channelId !== this.channelId) return;
// リプライなら再pack
@@ -52,7 +53,7 @@ export default class extends Channel {
}
@autobind
private onEvent(data: any) {
private onEvent(data: StreamMessages['channel']['spec']) {
if (data.type === 'typing') {
const id = data.body;
const begin = this.typers[id] == null;

View File

@@ -3,9 +3,9 @@ import { isMutedUserRelated } from '@/misc/is-muted-user-related';
import Channel from '../channel';
import { fetchMeta } from '@/misc/fetch-meta';
import { Notes } from '@/models/index';
import { PackedNote } from '@/models/repositories/note';
import { checkWordMute } from '@/misc/check-word-mute';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
import { Packed } from '@/misc/schema';
export default class extends Channel {
public readonly chName = 'globalTimeline';
@@ -24,7 +24,7 @@ export default class extends Channel {
}
@autobind
private async onNote(note: PackedNote) {
private async onNote(note: Packed<'Note'>) {
if (note.visibility !== 'public') return;
if (note.channelId != null) return;

View File

@@ -2,9 +2,9 @@ import autobind from 'autobind-decorator';
import { isMutedUserRelated } from '@/misc/is-muted-user-related';
import Channel from '../channel';
import { Notes } from '@/models/index';
import { PackedNote } from '@/models/repositories/note';
import { normalizeForSearch } from '@/misc/normalize-for-search';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
import { Packed } from '@/misc/schema';
export default class extends Channel {
public readonly chName = 'hashtag';
@@ -23,7 +23,7 @@ export default class extends Channel {
}
@autobind
private async onNote(note: PackedNote) {
private async onNote(note: Packed<'Note'>) {
const noteTags = note.tags ? note.tags.map((t: string) => t.toLowerCase()) : [];
const matched = this.q.some(tags => tags.every(tag => noteTags.includes(normalizeForSearch(tag))));
if (!matched) return;

View File

@@ -2,9 +2,9 @@ import autobind from 'autobind-decorator';
import { isMutedUserRelated } from '@/misc/is-muted-user-related';
import Channel from '../channel';
import { Notes } from '@/models/index';
import { PackedNote } from '@/models/repositories/note';
import { checkWordMute } from '@/misc/check-word-mute';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
import { Packed } from '@/misc/schema';
export default class extends Channel {
public readonly chName = 'homeTimeline';
@@ -18,7 +18,7 @@ export default class extends Channel {
}
@autobind
private async onNote(note: PackedNote) {
private async onNote(note: Packed<'Note'>) {
if (note.channelId) {
if (!this.followingChannels.has(note.channelId)) return;
} else {

View File

@@ -3,9 +3,9 @@ import { isMutedUserRelated } from '@/misc/is-muted-user-related';
import Channel from '../channel';
import { fetchMeta } from '@/misc/fetch-meta';
import { Notes } from '@/models/index';
import { PackedNote } from '@/models/repositories/note';
import { checkWordMute } from '@/misc/check-word-mute';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
import { Packed } from '@/misc/schema';
export default class extends Channel {
public readonly chName = 'hybridTimeline';
@@ -22,7 +22,7 @@ export default class extends Channel {
}
@autobind
private async onNote(note: PackedNote) {
private async onNote(note: Packed<'Note'>) {
// チャンネルの投稿ではなく、自分自身の投稿 または
// チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または
// チャンネルの投稿ではなく、全体公開のローカルの投稿 または

View File

@@ -3,9 +3,9 @@ import { isMutedUserRelated } from '@/misc/is-muted-user-related';
import Channel from '../channel';
import { fetchMeta } from '@/misc/fetch-meta';
import { Notes } from '@/models/index';
import { PackedNote } from '@/models/repositories/note';
import { checkWordMute } from '@/misc/check-word-mute';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
import { Packed } from '@/misc/schema';
export default class extends Channel {
public readonly chName = 'localTimeline';
@@ -24,7 +24,7 @@ export default class extends Channel {
}
@autobind
private async onNote(note: PackedNote) {
private async onNote(note: Packed<'Note'>) {
if (note.user.host !== null) return;
if (note.visibility !== 'public') return;
if (note.channelId != null && !this.followingChannels.has(note.channelId)) return;

View File

@@ -11,35 +11,33 @@ export default class extends Channel {
public async init(params: any) {
// Subscribe main stream channel
this.subscriber.on(`mainStream:${this.user!.id}`, async data => {
const { type } = data;
let { body } = data;
switch (type) {
switch (data.type) {
case 'notification': {
if (this.muting.has(body.userId)) return;
if (body.note && body.note.isHidden) {
const note = await Notes.pack(body.note.id, this.user, {
if (data.body.userId && this.muting.has(data.body.userId)) return;
if (data.body.note && data.body.note.isHidden) {
const note = await Notes.pack(data.body.note.id, this.user, {
detail: true
});
this.connection.cacheNote(note);
body.note = note;
data.body.note = note;
}
break;
}
case 'mention': {
if (this.muting.has(body.userId)) return;
if (body.isHidden) {
const note = await Notes.pack(body.id, this.user, {
if (this.muting.has(data.body.userId)) return;
if (data.body.isHidden) {
const note = await Notes.pack(data.body.id, this.user, {
detail: true
});
this.connection.cacheNote(note);
body = note;
data.body = note;
}
break;
}
}
this.send(type, body);
this.send(data.type, data.body);
});
}
}

View File

@@ -3,6 +3,8 @@ import { readUserMessagingMessage, readGroupMessagingMessage, deliverReadActivit
import Channel from '../channel';
import { UserGroupJoinings, Users, MessagingMessages } from '@/models/index';
import { User, ILocalUser, IRemoteUser } from '@/models/entities/user';
import { UserGroup } from '@/models/entities/user-group';
import { StreamMessages } from '../types';
export default class extends Channel {
public readonly chName = 'messaging';
@@ -12,7 +14,7 @@ export default class extends Channel {
private otherpartyId: string | null;
private otherparty: User | null;
private groupId: string | null;
private subCh: string;
private subCh: `messagingStream:${User['id']}-${User['id']}` | `messagingStream:${UserGroup['id']}`;
private typers: Record<User['id'], Date> = {};
private emitTypersIntervalId: ReturnType<typeof setInterval>;
@@ -45,7 +47,7 @@ export default class extends Channel {
}
@autobind
private onEvent(data: any) {
private onEvent(data: StreamMessages['messaging']['spec'] | StreamMessages['groupMessaging']['spec']) {
if (data.type === 'typing') {
const id = data.body;
const begin = this.typers[id] == null;

View File

@@ -3,8 +3,8 @@ import Channel from '../channel';
import { Notes, UserListJoinings, UserLists } from '@/models/index';
import { isMutedUserRelated } from '@/misc/is-muted-user-related';
import { User } from '@/models/entities/user';
import { PackedNote } from '@/models/repositories/note';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related';
import { Packed } from '@/misc/schema';
export default class extends Channel {
public readonly chName = 'userList';
@@ -47,7 +47,7 @@ export default class extends Channel {
}
@autobind
private async onNote(note: PackedNote) {
private async onNote(note: Packed<'Note'>) {
if (!this.listUsers.includes(note.userId)) return;
if (['followers', 'specified'].includes(note.visibility)) {

View File

@@ -14,7 +14,8 @@ import { AccessToken } from '@/models/entities/access-token';
import { UserProfile } from '@/models/entities/user-profile';
import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '@/services/stream';
import { UserGroup } from '@/models/entities/user-group';
import { PackedNote } from '@/models/repositories/note';
import { StreamEventEmitter, StreamMessages } from './types';
import { Packed } from '@/misc/schema';
/**
* Main stream connection
@@ -28,10 +29,10 @@ export default class Connection {
public followingChannels: Set<ChannelModel['id']> = new Set();
public token?: AccessToken;
private wsConnection: websocket.connection;
public subscriber: EventEmitter;
public subscriber: StreamEventEmitter;
private channels: Channel[] = [];
private subscribingNotes: any = {};
private cachedNotes: PackedNote[] = [];
private cachedNotes: Packed<'Note'>[] = [];
constructor(
wsConnection: websocket.connection,
@@ -46,8 +47,8 @@ export default class Connection {
this.wsConnection.on('message', this.onWsConnectionMessage);
this.subscriber.on('broadcast', async ({ type, body }) => {
this.onBroadcastMessage(type, body);
this.subscriber.on('broadcast', data => {
this.onBroadcastMessage(data);
});
if (this.user) {
@@ -57,43 +58,41 @@ export default class Connection {
this.updateFollowingChannels();
this.updateUserProfile();
this.subscriber.on(`user:${this.user.id}`, ({ type, body }) => {
this.onUserEvent(type, body);
});
this.subscriber.on(`user:${this.user.id}`, this.onUserEvent);
}
}
@autobind
private onUserEvent(type: string, body: any) {
switch (type) {
private onUserEvent(data: StreamMessages['user']['spec']) { // { type, body }と展開すると型も展開されてしまう
switch (data.type) {
case 'follow':
this.following.add(body.id);
this.following.add(data.body.id);
break;
case 'unfollow':
this.following.delete(body.id);
this.following.delete(data.body.id);
break;
case 'mute':
this.muting.add(body.id);
this.muting.add(data.body.id);
break;
case 'unmute':
this.muting.delete(body.id);
this.muting.delete(data.body.id);
break;
// TODO: block events
case 'followChannel':
this.followingChannels.add(body.id);
this.followingChannels.add(data.body.id);
break;
case 'unfollowChannel':
this.followingChannels.delete(body.id);
this.followingChannels.delete(data.body.id);
break;
case 'updateUserProfile':
this.userProfile = body;
this.userProfile = data.body;
break;
case 'terminate':
@@ -145,13 +144,13 @@ export default class Connection {
}
@autobind
private onBroadcastMessage(type: string, body: any) {
this.sendMessageToWs(type, body);
private onBroadcastMessage(data: StreamMessages['broadcast']['spec']) {
this.sendMessageToWs(data.type, data.body);
}
@autobind
public cacheNote(note: PackedNote) {
const add = (note: PackedNote) => {
public cacheNote(note: Packed<'Note'>) {
const add = (note: Packed<'Note'>) => {
const existIndex = this.cachedNotes.findIndex(n => n.id === note.id);
if (existIndex > -1) {
this.cachedNotes[existIndex] = note;
@@ -249,7 +248,7 @@ export default class Connection {
}
@autobind
private async onNoteStreamMessage(data: any) {
private async onNoteStreamMessage(data: StreamMessages['note']['spec']) {
this.sendMessageToWs('noteUpdated', {
id: data.body.id,
type: data.type,

View File

@@ -0,0 +1,299 @@
import { EventEmitter } from 'events';
import Emitter from 'strict-event-emitter-types';
import { Channel } from '@/models/entities/channel';
import { User } from '@/models/entities/user';
import { UserProfile } from '@/models/entities/user-profile';
import { Note } from '@/models/entities/note';
import { Antenna } from '@/models/entities/antenna';
import { DriveFile } from '@/models/entities/drive-file';
import { DriveFolder } from '@/models/entities/drive-folder';
import { Emoji } from '@/models/entities/emoji';
import { UserList } from '@/models/entities/user-list';
import { MessagingMessage } from '@/models/entities/messaging-message';
import { UserGroup } from '@/models/entities/user-group';
import { ReversiGame } from '@/models/entities/games/reversi/game';
import { AbuseUserReport } from '@/models/entities/abuse-user-report';
import { Signin } from '@/models/entities/signin';
import { Page } from '@/models/entities/page';
import { Packed } from '@/misc/schema';
//#region Stream type-body definitions
export interface InternalStreamTypes {
antennaCreated: Antenna;
antennaDeleted: Antenna;
antennaUpdated: Antenna;
}
export interface BroadcastTypes {
emojiAdded: {
emoji: Packed<'Emoji'>;
};
}
export interface UserStreamTypes {
terminate: {};
followChannel: Channel;
unfollowChannel: Channel;
updateUserProfile: UserProfile;
mute: User;
unmute: User;
follow: Packed<'User'>;
unfollow: Packed<'User'>;
userAdded: Packed<'User'>;
}
export interface MainStreamTypes {
notification: Packed<'Notification'>;
mention: Packed<'Note'>;
reply: Packed<'Note'>;
renote: Packed<'Note'>;
follow: Packed<'User'>;
followed: Packed<'User'>;
unfollow: Packed<'User'>;
meUpdated: Packed<'User'>;
pageEvent: {
pageId: Page['id'];
event: string;
var: any;
userId: User['id'];
user: Packed<'User'>;
};
urlUploadFinished: {
marker?: string | null;
file: Packed<'DriveFile'>;
};
readAllNotifications: undefined;
unreadNotification: Packed<'Notification'>;
unreadMention: Note['id'];
readAllUnreadMentions: undefined;
unreadSpecifiedNote: Note['id'];
readAllUnreadSpecifiedNotes: undefined;
readAllMessagingMessages: undefined;
messagingMessage: Packed<'MessagingMessage'>;
unreadMessagingMessage: Packed<'MessagingMessage'>;
readAllAntennas: undefined;
unreadAntenna: Antenna;
readAllAnnouncements: undefined;
readAllChannels: undefined;
unreadChannel: Note['id'];
myTokenRegenerated: undefined;
reversiNoInvites: undefined;
reversiInvited: Packed<'ReversiMatching'>;
signin: Signin;
registryUpdated: {
scope?: string[];
key: string;
value: any | null;
};
driveFileCreated: Packed<'DriveFile'>;
readAntenna: Antenna;
}
export interface DriveStreamTypes {
fileCreated: Packed<'DriveFile'>;
fileDeleted: DriveFile['id'];
fileUpdated: Packed<'DriveFile'>;
folderCreated: Packed<'DriveFolder'>;
folderDeleted: DriveFolder['id'];
folderUpdated: Packed<'DriveFolder'>;
}
export interface NoteStreamTypes {
pollVoted: {
choice: number;
userId: User['id'];
};
deleted: {
deletedAt: Date;
};
reacted: {
reaction: string;
emoji?: Emoji;
userId: User['id'];
};
unreacted: {
reaction: string;
userId: User['id'];
};
}
type NoteStreamEventTypes = {
[key in keyof NoteStreamTypes]: {
id: Note['id'];
body: NoteStreamTypes[key];
};
};
export interface ChannelStreamTypes {
typing: User['id'];
}
export interface UserListStreamTypes {
userAdded: Packed<'User'>;
userRemoved: Packed<'User'>;
}
export interface AntennaStreamTypes {
note: Note;
}
export interface MessagingStreamTypes {
read: MessagingMessage['id'][];
typing: User['id'];
message: Packed<'MessagingMessage'>;
deleted: MessagingMessage['id'];
}
export interface GroupMessagingStreamTypes {
read: {
ids: MessagingMessage['id'][];
userId: User['id'];
};
typing: User['id'];
message: Packed<'MessagingMessage'>;
deleted: MessagingMessage['id'];
}
export interface MessagingIndexStreamTypes {
read: MessagingMessage['id'][];
message: Packed<'MessagingMessage'>;
}
export interface ReversiStreamTypes {
matched: Packed<'ReversiGame'>;
invited: Packed<'ReversiMatching'>;
}
export interface ReversiGameStreamTypes {
started: Packed<'ReversiGame'>;
ended: {
winnerId?: User['id'] | null,
game: Packed<'ReversiGame'>;
};
updateSettings: {
key: string;
value: FIXME;
};
initForm: {
userId: User['id'];
form: FIXME;
};
updateForm: {
userId: User['id'];
id: string;
value: FIXME;
};
message: {
userId: User['id'];
message: FIXME;
};
changeAccepts: {
user1: boolean;
user2: boolean;
};
set: {
at: Date;
color: boolean;
pos: number;
next: boolean;
};
watching: User['id'];
}
export interface AdminStreamTypes {
newAbuseUserReport: {
id: AbuseUserReport['id'];
targetUserId: User['id'],
reporterId: User['id'],
comment: string;
};
}
//#endregion
// 辞書(interface or type)から{ type, body }ユニオンを定義
// https://stackoverflow.com/questions/49311989/can-i-infer-the-type-of-a-value-using-extends-keyof-type
// VS Codeの展開を防止するためにEvents型を定義
type Events<T extends object> = { [K in keyof T]: { type: K; body: T[K]; } };
type EventUnionFromDictionary<
T extends object,
U = Events<T>
> = U[keyof U];
// name/messages(spec) pairs dictionary
export type StreamMessages = {
internal: {
name: 'internal';
spec: EventUnionFromDictionary<InternalStreamTypes>;
};
broadcast: {
name: 'broadcast';
spec: EventUnionFromDictionary<BroadcastTypes>;
};
user: {
name: `user:${User['id']}`;
spec: EventUnionFromDictionary<UserStreamTypes>;
};
main: {
name: `mainStream:${User['id']}`;
spec: EventUnionFromDictionary<MainStreamTypes>;
};
drive: {
name: `driveStream:${User['id']}`;
spec: EventUnionFromDictionary<DriveStreamTypes>;
};
note: {
name: `noteStream:${Note['id']}`;
spec: EventUnionFromDictionary<NoteStreamEventTypes>;
};
channel: {
name: `channelStream:${Channel['id']}`;
spec: EventUnionFromDictionary<ChannelStreamTypes>;
};
userList: {
name: `userListStream:${UserList['id']}`;
spec: EventUnionFromDictionary<UserListStreamTypes>;
};
antenna: {
name: `antennaStream:${Antenna['id']}`;
spec: EventUnionFromDictionary<AntennaStreamTypes>;
};
messaging: {
name: `messagingStream:${User['id']}-${User['id']}`;
spec: EventUnionFromDictionary<MessagingStreamTypes>;
};
groupMessaging: {
name: `messagingStream:${UserGroup['id']}`;
spec: EventUnionFromDictionary<GroupMessagingStreamTypes>;
};
messagingIndex: {
name: `messagingIndexStream:${User['id']}`;
spec: EventUnionFromDictionary<MessagingIndexStreamTypes>;
};
reversi: {
name: `reversiStream:${User['id']}`;
spec: EventUnionFromDictionary<ReversiStreamTypes>;
};
reversiGame: {
name: `reversiGameStream:${ReversiGame['id']}`;
spec: EventUnionFromDictionary<ReversiGameStreamTypes>;
};
admin: {
name: `adminStream:${User['id']}`;
spec: EventUnionFromDictionary<AdminStreamTypes>;
};
notes: {
name: 'notesStream';
spec: Packed<'Note'>;
};
};
// API event definitions
// ストリームごとのEmitterの辞書を用意
type EventEmitterDictionary = { [x in keyof StreamMessages]: Emitter<EventEmitter, { [y in StreamMessages[x]['name']]: (e: StreamMessages[x]['spec']) => void }> };
// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする
export type StreamEventEmitter = UnionToIntersection<EventEmitterDictionary[keyof StreamMessages]>;
// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる
// provide stream channels union
export type StreamChannels = StreamMessages[keyof StreamMessages]['name'];