diff --git a/package.json b/package.json index 514da0c7cc..0a28fea05b 100644 --- a/package.json +++ b/package.json @@ -209,6 +209,7 @@ "seedrandom": "3.0.5", "sharp": "0.29.1", "speakeasy": "2.0.0", + "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", "style-loader": "3.2.1", "summaly": "2.4.1", diff --git a/src/misc/check-hit-antenna.ts b/src/misc/check-hit-antenna.ts index 38965f4b0d..3789054b26 100644 --- a/src/misc/check-hit-antenna.ts +++ b/src/misc/check-hit-antenna.ts @@ -3,13 +3,13 @@ import { Note } from '@/models/entities/note'; import { User } from '@/models/entities/user'; import { UserListJoinings, UserGroupJoinings } from '@/models/index'; import { getFullApAccount } from './convert-host'; -import { PackedNote } from '../models/repositories/note'; import { parseAcct } from '@/misc/acct'; +import { Packed } from './schema'; /** * noteUserFollowers / antennaUserFollowing はどちらか一方が指定されていればよい */ -export async function checkHitAntenna(antenna: Antenna, note: (Note | PackedNote), noteUser: { username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise { +export async function checkHitAntenna(antenna: Antenna, note: (Note | Packed<'Note'>), noteUser: { username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise { if (note.visibility === 'specified') return false; if (note.visibility === 'followers') { diff --git a/src/misc/schema.ts b/src/misc/schema.ts index d27c9eff99..4131875ef7 100644 --- a/src/misc/schema.ts +++ b/src/misc/schema.ts @@ -21,6 +21,9 @@ import { packedClipSchema } from '@/models/repositories/clip'; import { packedFederationInstanceSchema } from '@/models/repositories/federation-instance'; import { packedQueueCountSchema } from '@/models/repositories/queue'; import { packedGalleryPostSchema } from '@/models/repositories/gallery-post'; +import { packedEmojiSchema } from '@/models/repositories/emoji'; +import { packedReversiGameSchema } from '@/models/repositories/games/reversi/game'; +import { packedReversiMatchingSchema } from '@/models/repositories/games/reversi/matching'; export const refs = { User: packedUserSchema, @@ -45,8 +48,13 @@ export const refs = { Clip: packedClipSchema, FederationInstance: packedFederationInstanceSchema, GalleryPost: packedGalleryPostSchema, + Emoji: packedEmojiSchema, + ReversiGame: packedReversiGameSchema, + ReversiMatching: packedReversiMatchingSchema, }; +export type Packed = ObjType<(typeof refs[x])['properties']>; + export interface Schema extends SimpleSchema { items?: Schema; properties?: Obj; @@ -92,7 +100,7 @@ export type SchemaType

= p['type'] extends 'array' ? NullOrUndefined>[]> : p['type'] extends 'object' ? ( p['ref'] extends keyof typeof refs - ? NullOrUndefined> + ? NullOrUndefined> : NullOrUndefined>> ) : p['type'] extends 'any' ? NullOrUndefined : diff --git a/src/models/repositories/antenna.ts b/src/models/repositories/antenna.ts index e61eed5e08..657de55581 100644 --- a/src/models/repositories/antenna.ts +++ b/src/models/repositories/antenna.ts @@ -1,15 +1,13 @@ import { EntityRepository, Repository } from 'typeorm'; import { Antenna } from '@/models/entities/antenna'; -import { SchemaType } from '@/misc/schema'; +import { Packed } from '@/misc/schema'; import { AntennaNotes, UserGroupJoinings } from '../index'; -export type PackedAntenna = SchemaType; - @EntityRepository(Antenna) export class AntennaRepository extends Repository { public async pack( src: Antenna['id'] | Antenna, - ): Promise { + ): Promise> { const antenna = typeof src === 'object' ? src : await this.findOneOrFail(src); const hasUnreadNote = (await AntennaNotes.findOne({ antennaId: antenna.id, read: false })) != null; diff --git a/src/models/repositories/app.ts b/src/models/repositories/app.ts index 2287bd4390..0226edad11 100644 --- a/src/models/repositories/app.ts +++ b/src/models/repositories/app.ts @@ -1,9 +1,8 @@ import { EntityRepository, Repository } from 'typeorm'; import { App } from '@/models/entities/app'; import { AccessTokens } from '../index'; -import { SchemaType } from '@/misc/schema'; - -export type PackedApp = SchemaType; +import { Packed } from '@/misc/schema'; +import { User } from '../entities/user'; @EntityRepository(App) export class AppRepository extends Repository { @@ -15,7 +14,7 @@ export class AppRepository extends Repository { includeSecret?: boolean, includeProfileImageIds?: boolean } - ): Promise { + ): Promise> { const opts = Object.assign({ detail: false, includeSecret: false, @@ -52,13 +51,9 @@ export const packedAppSchema = { type: 'string' as const, optional: false as const, nullable: false as const }, - createdAt: { + callbackUrl: { type: 'string' as const, - optional: false as const, nullable: false as const - }, - lastUsedAt: { - type: 'string' as const, - optional: false as const, nullable: false as const + optional: false as const, nullable: true as const }, permission: { type: 'array' as const, diff --git a/src/models/repositories/blocking.ts b/src/models/repositories/blocking.ts index 515b3a6b16..ac60c9a4ce 100644 --- a/src/models/repositories/blocking.ts +++ b/src/models/repositories/blocking.ts @@ -2,17 +2,15 @@ import { EntityRepository, Repository } from 'typeorm'; import { Users } from '../index'; import { Blocking } from '@/models/entities/blocking'; import { awaitAll } from '@/prelude/await-all'; -import { SchemaType } from '@/misc/schema'; +import { Packed } from '@/misc/schema'; import { User } from '@/models/entities/user'; -export type PackedBlocking = SchemaType; - @EntityRepository(Blocking) export class BlockingRepository extends Repository { public async pack( src: Blocking['id'] | Blocking, me?: { id: User['id'] } | null | undefined - ): Promise { + ): Promise> { const blocking = typeof src === 'object' ? src : await this.findOneOrFail(src); return await awaitAll({ diff --git a/src/models/repositories/channel.ts b/src/models/repositories/channel.ts index 4bb829f570..5c7d095473 100644 --- a/src/models/repositories/channel.ts +++ b/src/models/repositories/channel.ts @@ -1,17 +1,15 @@ import { EntityRepository, Repository } from 'typeorm'; import { Channel } from '@/models/entities/channel'; -import { SchemaType } from '@/misc/schema'; +import { Packed } from '@/misc/schema'; import { DriveFiles, ChannelFollowings, NoteUnreads } from '../index'; import { User } from '@/models/entities/user'; -export type PackedChannel = SchemaType; - @EntityRepository(Channel) export class ChannelRepository extends Repository { public async pack( src: Channel['id'] | Channel, me?: { id: User['id'] } | null | undefined, - ): Promise { + ): Promise> { const channel = typeof src === 'object' ? src : await this.findOneOrFail(src); const meId = me ? me.id : null; diff --git a/src/models/repositories/clip.ts b/src/models/repositories/clip.ts index e3d718bef4..7892811d48 100644 --- a/src/models/repositories/clip.ts +++ b/src/models/repositories/clip.ts @@ -1,16 +1,14 @@ import { EntityRepository, Repository } from 'typeorm'; import { Clip } from '@/models/entities/clip'; -import { SchemaType } from '@/misc/schema'; +import { Packed } from '@/misc/schema'; import { Users } from '../index'; import { awaitAll } from '@/prelude/await-all'; -export type PackedClip = SchemaType; - @EntityRepository(Clip) export class ClipRepository extends Repository { public async pack( src: Clip['id'] | Clip, - ): Promise { + ): Promise> { const clip = typeof src === 'object' ? src : await this.findOneOrFail(src); return await awaitAll({ diff --git a/src/models/repositories/drive-file.ts b/src/models/repositories/drive-file.ts index 63bd020cbe..ddf9a46afd 100644 --- a/src/models/repositories/drive-file.ts +++ b/src/models/repositories/drive-file.ts @@ -4,14 +4,12 @@ import { Users, DriveFolders } from '../index'; import { User } from '@/models/entities/user'; import { toPuny } from '@/misc/convert-host'; import { awaitAll } from '@/prelude/await-all'; -import { SchemaType } from '@/misc/schema'; +import { Packed } from '@/misc/schema'; import config from '@/config/index'; import { query, appendQuery } from '@/prelude/url'; import { Meta } from '@/models/entities/meta'; import { fetchMeta } from '@/misc/fetch-meta'; -export type PackedDriveFile = SchemaType; - type PackOptions = { detail?: boolean, self?: boolean, @@ -99,12 +97,12 @@ export class DriveFileRepository extends Repository { return parseInt(sum, 10) || 0; } - public async pack(src: DriveFile['id'], options?: PackOptions): Promise; - public async pack(src: DriveFile, options?: PackOptions): Promise; + public async pack(src: DriveFile['id'], options?: PackOptions): Promise | null>; + public async pack(src: DriveFile, options?: PackOptions): Promise>; public async pack( src: DriveFile['id'] | DriveFile, options?: PackOptions - ): Promise { + ): Promise | null> { const opts = Object.assign({ detail: false, self: false diff --git a/src/models/repositories/drive-folder.ts b/src/models/repositories/drive-folder.ts index bc73018f29..8ef6f01b5d 100644 --- a/src/models/repositories/drive-folder.ts +++ b/src/models/repositories/drive-folder.ts @@ -2,9 +2,7 @@ import { EntityRepository, Repository } from 'typeorm'; import { DriveFolders, DriveFiles } from '../index'; import { DriveFolder } from '@/models/entities/drive-folder'; import { awaitAll } from '@/prelude/await-all'; -import { SchemaType } from '@/misc/schema'; - -export type PackedDriveFolder = SchemaType; +import { Packed } from '@/misc/schema'; @EntityRepository(DriveFolder) export class DriveFolderRepository extends Repository { @@ -20,7 +18,7 @@ export class DriveFolderRepository extends Repository { options?: { detail: boolean } - ): Promise { + ): Promise> { const opts = Object.assign({ detail: false }, options); diff --git a/src/models/repositories/emoji.ts b/src/models/repositories/emoji.ts index c3d7184ec9..7985c27aba 100644 --- a/src/models/repositories/emoji.ts +++ b/src/models/repositories/emoji.ts @@ -1,11 +1,12 @@ import { EntityRepository, Repository } from 'typeorm'; import { Emoji } from '@/models/entities/emoji'; +import { Packed } from '@/misc/schema'; @EntityRepository(Emoji) export class EmojiRepository extends Repository { public async pack( src: Emoji['id'] | Emoji, - ) { + ): Promise> { const emoji = typeof src === 'object' ? src : await this.findOneOrFail(src); return { @@ -24,3 +25,41 @@ export class EmojiRepository extends Repository { return Promise.all(emojis.map(x => this.pack(x))); } } + +export const packedEmojiSchema = { + type: 'object' as const, + optional: false as const, nullable: false as const, + properties: { + id: { + type: 'string' as const, + optional: false as const, nullable: false as const, + format: 'id', + example: 'xxxxxxxxxx', + }, + aliases: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'string' as const, + optional: false as const, nullable: false as const, + format: 'id', + }, + }, + name: { + type: 'string' as const, + optional: false as const, nullable: false as const, + }, + category: { + type: 'string' as const, + optional: false as const, nullable: true as const, + }, + host: { + type: 'string' as const, + optional: false as const, nullable: true as const, + }, + url: { + type: 'string' as const, + optional: false as const, nullable: false as const, + }, + } +}; diff --git a/src/models/repositories/following.ts b/src/models/repositories/following.ts index 24ddd0d676..b1f716069f 100644 --- a/src/models/repositories/following.ts +++ b/src/models/repositories/following.ts @@ -2,7 +2,7 @@ import { EntityRepository, Repository } from 'typeorm'; import { Users } from '../index'; import { Following } from '@/models/entities/following'; import { awaitAll } from '@/prelude/await-all'; -import { SchemaType } from '@/misc/schema'; +import { Packed } from '@/misc/schema'; import { User } from '@/models/entities/user'; type LocalFollowerFollowing = Following & { @@ -29,8 +29,6 @@ type RemoteFolloweeFollowing = Following & { followeeSharedInbox: string; }; -export type PackedFollowing = SchemaType; - @EntityRepository(Following) export class FollowingRepository extends Repository { public isLocalFollower(following: Following): following is LocalFollowerFollowing { @@ -56,7 +54,7 @@ export class FollowingRepository extends Repository { populateFollowee?: boolean; populateFollower?: boolean; } - ): Promise { + ): Promise> { const following = typeof src === 'object' ? src : await this.findOneOrFail(src); if (opts == null) opts = {}; diff --git a/src/models/repositories/gallery-post.ts b/src/models/repositories/gallery-post.ts index afa22e9edf..4f666ff252 100644 --- a/src/models/repositories/gallery-post.ts +++ b/src/models/repositories/gallery-post.ts @@ -1,18 +1,16 @@ import { EntityRepository, Repository } from 'typeorm'; import { GalleryPost } from '@/models/entities/gallery-post'; -import { SchemaType } from '@/misc/schema'; +import { Packed } from '@/misc/schema'; import { Users, DriveFiles, GalleryLikes } from '../index'; import { awaitAll } from '@/prelude/await-all'; import { User } from '@/models/entities/user'; -export type PackedGalleryPost = SchemaType; - @EntityRepository(GalleryPost) export class GalleryPostRepository extends Repository { public async pack( src: GalleryPost['id'] | GalleryPost, me?: { id: User['id'] } | null | undefined, - ): Promise { + ): Promise> { const meId = me ? me.id : null; const post = typeof src === 'object' ? src : await this.findOneOrFail(src); diff --git a/src/models/repositories/games/reversi/game.ts b/src/models/repositories/games/reversi/game.ts index dc91ad51b8..9adb386fa9 100644 --- a/src/models/repositories/games/reversi/game.ts +++ b/src/models/repositories/games/reversi/game.ts @@ -2,6 +2,7 @@ import { User } from '@/models/entities/user'; import { EntityRepository, Repository } from 'typeorm'; import { Users } from '../../../index'; import { ReversiGame } from '@/models/entities/games/reversi/game'; +import { Packed } from '@/misc/schema'; @EntityRepository(ReversiGame) export class ReversiGameRepository extends Repository { @@ -11,7 +12,7 @@ export class ReversiGameRepository extends Repository { options?: { detail?: boolean } - ) { + ): Promise> { const opts = Object.assign({ detail: true }, options); @@ -20,8 +21,8 @@ export class ReversiGameRepository extends Repository { return { id: game.id, - createdAt: game.createdAt, - startedAt: game.startedAt, + createdAt: game.createdAt.toISOString(), + startedAt: game.startedAt && game.startedAt.toISOString(), isStarted: game.isStarted, isEnded: game.isEnded, form1: game.form1, @@ -41,9 +42,150 @@ export class ReversiGameRepository extends Repository { canPutEverywhere: game.canPutEverywhere, loopedBoard: game.loopedBoard, ...(opts.detail ? { - logs: game.logs, + logs: game.logs.map(log => ({ + at: log.at.toISOString(), + color: log.color, + pos: log.pos + })), map: game.map, } : {}) }; } } + +export const packedReversiGameSchema = { + type: 'object' as const, + optional: false as const, nullable: false as const, + properties: { + id: { + type: 'string' as const, + optional: false as const, nullable: false as const, + format: 'id', + example: 'xxxxxxxxxx', + }, + createdAt: { + type: 'string' as const, + optional: false as const, nullable: false as const, + format: 'date-time', + }, + startedAt: { + type: 'string' as const, + optional: false as const, nullable: true as const, + format: 'date-time', + }, + isStarted: { + type: 'boolean' as const, + optional: false as const, nullable: false as const, + }, + isEnded: { + type: 'boolean' as const, + optional: false as const, nullable: false as const, + }, + form1: { + type: 'any' as const, + optional: false as const, nullable: true as const, + }, + form2: { + type: 'any' as const, + optional: false as const, nullable: true as const, + }, + user1Accepted: { + type: 'boolean' as const, + optional: false as const, nullable: false as const, + }, + user2Accepted: { + type: 'boolean' as const, + optional: false as const, nullable: false as const, + }, + user1Id: { + type: 'string' as const, + optional: false as const, nullable: false as const, + format: 'id', + example: 'xxxxxxxxxx', + }, + user2Id: { + type: 'string' as const, + optional: false as const, nullable: false as const, + format: 'id', + example: 'xxxxxxxxxx', + }, + user1: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'User' as const, + }, + user2: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'User' as const, + }, + winnerId: { + type: 'string' as const, + optional: false as const, nullable: true as const, + format: 'id', + example: 'xxxxxxxxxx', + }, + winner: { + type: 'object' as const, + optional: false as const, nullable: true as const, + ref: 'User' as const, + }, + surrendered: { + type: 'string' as const, + optional: false as const, nullable: true as const, + format: 'id', + example: 'xxxxxxxxxx', + }, + black: { + type: 'number' as const, + optional: false as const, nullable: true as const, + }, + bw: { + type: 'string' as const, + optional: false as const, nullable: false as const, + }, + isLlotheo: { + type: 'boolean' as const, + optional: false as const, nullable: false as const, + }, + canPutEverywhere: { + type: 'boolean' as const, + optional: false as const, nullable: false as const, + }, + loopedBoard: { + type: 'boolean' as const, + optional: false as const, nullable: false as const, + }, + logs: { + type: 'array' as const, + optional: true as const, nullable: false as const, + items: { + type: 'object' as const, + optional: true as const, nullable: false as const, + properties: { + at: { + type: 'string' as const, + optional: false as const, nullable: false as const, + format: 'date-time', + }, + color: { + type: 'boolean' as const, + optional: false as const, nullable: false as const, + }, + pos: { + type: 'number' as const, + optional: false as const, nullable: false as const, + }, + } + } + }, + map: { + type: 'array' as const, + optional: true as const, nullable: false as const, + items: { + type: 'string' as const, + optional: false as const, nullable: false as const, + } + } + } +}; diff --git a/src/models/repositories/games/reversi/matching.ts b/src/models/repositories/games/reversi/matching.ts index 148221dee5..b4515800df 100644 --- a/src/models/repositories/games/reversi/matching.ts +++ b/src/models/repositories/games/reversi/matching.ts @@ -3,18 +3,19 @@ import { ReversiMatching } from '@/models/entities/games/reversi/matching'; import { Users } from '../../../index'; import { awaitAll } from '@/prelude/await-all'; import { User } from '@/models/entities/user'; +import { Packed } from '@/misc/schema'; @EntityRepository(ReversiMatching) export class ReversiMatchingRepository extends Repository { public async pack( src: ReversiMatching['id'] | ReversiMatching, me: { id: User['id'] } - ) { + ): Promise> { const matching = typeof src === 'object' ? src : await this.findOneOrFail(src); return await awaitAll({ id: matching.id, - createdAt: matching.createdAt, + createdAt: matching.createdAt.toISOString(), parentId: matching.parentId, parent: Users.pack(matching.parentId, me, { detail: true @@ -26,3 +27,43 @@ export class ReversiMatchingRepository extends Repository { }); } } + +export const packedReversiMatchingSchema = { + type: 'object' as const, + optional: false as const, nullable: false as const, + properties: { + id: { + type: 'string' as const, + optional: false as const, nullable: false as const, + format: 'id', + example: 'xxxxxxxxxx', + }, + createdAt: { + type: 'string' as const, + optional: false as const, nullable: false as const, + format: 'date-time', + }, + parentId: { + type: 'string' as const, + optional: false as const, nullable: false as const, + format: 'id', + example: 'xxxxxxxxxx', + }, + parent: { + type: 'object' as const, + optional: false as const, nullable: true as const, + ref: 'User' as const, + }, + childId: { + type: 'string' as const, + optional: false as const, nullable: false as const, + format: 'id', + example: 'xxxxxxxxxx', + }, + child: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'User' as const, + }, + } +}; diff --git a/src/models/repositories/hashtag.ts b/src/models/repositories/hashtag.ts index ee42ad16b6..d52f6ba7c6 100644 --- a/src/models/repositories/hashtag.ts +++ b/src/models/repositories/hashtag.ts @@ -1,14 +1,12 @@ import { EntityRepository, Repository } from 'typeorm'; import { Hashtag } from '@/models/entities/hashtag'; -import { SchemaType } from '@/misc/schema'; - -export type PackedHashtag = SchemaType; +import { Packed } from '@/misc/schema'; @EntityRepository(Hashtag) export class HashtagRepository extends Repository { public async pack( src: Hashtag, - ): Promise { + ): Promise> { return { tag: src.name, mentionedUsersCount: src.mentionedUsersCount, diff --git a/src/models/repositories/messaging-message.ts b/src/models/repositories/messaging-message.ts index f97905af2f..abdff63689 100644 --- a/src/models/repositories/messaging-message.ts +++ b/src/models/repositories/messaging-message.ts @@ -1,11 +1,9 @@ import { EntityRepository, Repository } from 'typeorm'; import { MessagingMessage } from '@/models/entities/messaging-message'; import { Users, DriveFiles, UserGroups } from '../index'; -import { SchemaType } from '@/misc/schema'; +import { Packed } from '@/misc/schema'; import { User } from '@/models/entities/user'; -export type PackedMessagingMessage = SchemaType; - @EntityRepository(MessagingMessage) export class MessagingMessageRepository extends Repository { public validateText(text: string): boolean { @@ -19,7 +17,7 @@ export class MessagingMessageRepository extends Repository { populateRecipient?: boolean, populateGroup?: boolean, } - ): Promise { + ): Promise> { const opts = options || { populateRecipient: true, populateGroup: true, diff --git a/src/models/repositories/muting.ts b/src/models/repositories/muting.ts index d957b1792d..869afd3c4e 100644 --- a/src/models/repositories/muting.ts +++ b/src/models/repositories/muting.ts @@ -2,17 +2,15 @@ import { EntityRepository, Repository } from 'typeorm'; import { Users } from '../index'; import { Muting } from '@/models/entities/muting'; import { awaitAll } from '@/prelude/await-all'; -import { SchemaType } from '@/misc/schema'; +import { Packed } from '@/misc/schema'; import { User } from '@/models/entities/user'; -export type PackedMuting = SchemaType; - @EntityRepository(Muting) export class MutingRepository extends Repository { public async pack( src: Muting['id'] | Muting, me?: { id: User['id'] } | null | undefined - ): Promise { + ): Promise> { const muting = typeof src === 'object' ? src : await this.findOneOrFail(src); return await awaitAll({ diff --git a/src/models/repositories/note-reaction.ts b/src/models/repositories/note-reaction.ts index e73a832109..ba74076f6c 100644 --- a/src/models/repositories/note-reaction.ts +++ b/src/models/repositories/note-reaction.ts @@ -1,18 +1,16 @@ import { EntityRepository, Repository } from 'typeorm'; import { NoteReaction } from '@/models/entities/note-reaction'; import { Users } from '../index'; -import { SchemaType } from '@/misc/schema'; +import { Packed } from '@/misc/schema'; import { convertLegacyReaction } from '@/misc/reaction-lib'; import { User } from '@/models/entities/user'; -export type PackedNoteReaction = SchemaType; - @EntityRepository(NoteReaction) export class NoteReactionRepository extends Repository { public async pack( src: NoteReaction['id'] | NoteReaction, me?: { id: User['id'] } | null | undefined - ): Promise { + ): Promise> { const reaction = typeof src === 'object' ? src : await this.findOneOrFail(src); return { diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts index 376a09d0c6..c0ac22b2db 100644 --- a/src/models/repositories/note.ts +++ b/src/models/repositories/note.ts @@ -3,15 +3,13 @@ import * as mfm from 'mfm-js'; import { Note } from '@/models/entities/note'; import { User } from '@/models/entities/user'; import { Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls, Channels } from '../index'; -import { SchemaType } from '@/misc/schema'; +import { Packed } from '@/misc/schema'; import { nyaize } from '@/misc/nyaize'; import { awaitAll } from '@/prelude/await-all'; import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '@/misc/reaction-lib'; import { NoteReaction } from '@/models/entities/note-reaction'; import { aggregateNoteEmojis, populateEmojis, prefetchEmojis } from '@/misc/populate-emojis'; -export type PackedNote = SchemaType; - @EntityRepository(Note) export class NoteRepository extends Repository { public validateCw(x: string) { @@ -67,7 +65,7 @@ export class NoteRepository extends Repository { return true; } - private async hideNote(packedNote: PackedNote, meId: User['id'] | null) { + private async hideNote(packedNote: Packed<'Note'>, meId: User['id'] | null) { // TODO: isVisibleForMe を使うようにしても良さそう(型違うけど) let hide = false; @@ -137,7 +135,7 @@ export class NoteRepository extends Repository { myReactions: Map; }; } - ): Promise { + ): Promise> { const opts = Object.assign({ detail: true, skipHide: false diff --git a/src/models/repositories/notification.ts b/src/models/repositories/notification.ts index b7f9e3643c..d1cf9b087e 100644 --- a/src/models/repositories/notification.ts +++ b/src/models/repositories/notification.ts @@ -2,15 +2,13 @@ import { EntityRepository, In, Repository } from 'typeorm'; import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '../index'; import { Notification } from '@/models/entities/notification'; import { awaitAll } from '@/prelude/await-all'; -import { SchemaType } from '@/misc/schema'; +import { Packed } from '@/misc/schema'; import { Note } from '@/models/entities/note'; import { NoteReaction } from '@/models/entities/note-reaction'; import { User } from '@/models/entities/user'; import { aggregateNoteEmojis, prefetchEmojis } from '@/misc/populate-emojis'; import { notificationTypes } from '@/types'; -export type PackedNotification = SchemaType; - @EntityRepository(Notification) export class NotificationRepository extends Repository { public async pack( @@ -20,7 +18,7 @@ export class NotificationRepository extends Repository { myReactions: Map; }; } - ): Promise { + ): Promise> { const notification = typeof src === 'object' ? src : await this.findOneOrFail(src); const token = notification.appAccessTokenId ? await AccessTokens.findOneOrFail(notification.appAccessTokenId) : null; diff --git a/src/models/repositories/page.ts b/src/models/repositories/page.ts index 1a61e2c99c..3a3642d7ec 100644 --- a/src/models/repositories/page.ts +++ b/src/models/repositories/page.ts @@ -1,19 +1,17 @@ import { EntityRepository, Repository } from 'typeorm'; import { Page } from '@/models/entities/page'; -import { SchemaType } from '@/misc/schema'; +import { Packed } from '@/misc/schema'; import { Users, DriveFiles, PageLikes } from '../index'; import { awaitAll } from '@/prelude/await-all'; import { DriveFile } from '@/models/entities/drive-file'; import { User } from '@/models/entities/user'; -export type PackedPage = SchemaType; - @EntityRepository(Page) export class PageRepository extends Repository { public async pack( src: Page['id'] | Page, me?: { id: User['id'] } | null | undefined, - ): Promise { + ): Promise> { const meId = me ? me.id : null; const page = typeof src === 'object' ? src : await this.findOneOrFail(src); diff --git a/src/models/repositories/signin.ts b/src/models/repositories/signin.ts index 9942d2d962..f375f9b5c0 100644 --- a/src/models/repositories/signin.ts +++ b/src/models/repositories/signin.ts @@ -4,7 +4,7 @@ import { Signin } from '@/models/entities/signin'; @EntityRepository(Signin) export class SigninRepository extends Repository { public async pack( - src: any, + src: Signin, ) { return src; } diff --git a/src/models/repositories/user-group.ts b/src/models/repositories/user-group.ts index a76ac7b9d3..b38a2fb50d 100644 --- a/src/models/repositories/user-group.ts +++ b/src/models/repositories/user-group.ts @@ -1,15 +1,13 @@ import { EntityRepository, Repository } from 'typeorm'; import { UserGroup } from '@/models/entities/user-group'; import { UserGroupJoinings } from '../index'; -import { SchemaType } from '@/misc/schema'; - -export type PackedUserGroup = SchemaType; +import { Packed } from '@/misc/schema'; @EntityRepository(UserGroup) export class UserGroupRepository extends Repository { public async pack( src: UserGroup['id'] | UserGroup, - ): Promise { + ): Promise> { const userGroup = typeof src === 'object' ? src : await this.findOneOrFail(src); const users = await UserGroupJoinings.find({ diff --git a/src/models/repositories/user-list.ts b/src/models/repositories/user-list.ts index 809dbe0268..331c278e6f 100644 --- a/src/models/repositories/user-list.ts +++ b/src/models/repositories/user-list.ts @@ -1,15 +1,13 @@ import { EntityRepository, Repository } from 'typeorm'; import { UserList } from '@/models/entities/user-list'; import { UserListJoinings } from '../index'; -import { SchemaType } from '@/misc/schema'; - -export type PackedUserList = SchemaType; +import { Packed } from '@/misc/schema'; @EntityRepository(UserList) export class UserListRepository extends Repository { public async pack( src: UserList['id'] | UserList, - ): Promise { + ): Promise> { const userList = typeof src === 'object' ? src : await this.findOneOrFail(src); const users = await UserListJoinings.find({ diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts index 39c90cf5ed..b6f27e32e2 100644 --- a/src/models/repositories/user.ts +++ b/src/models/repositories/user.ts @@ -3,14 +3,12 @@ import { EntityRepository, Repository, In, Not } from 'typeorm'; import { User, ILocalUser, IRemoteUser } from '@/models/entities/user'; import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances } from '../index'; import config from '@/config/index'; -import { SchemaType } from '@/misc/schema'; +import { Packed } from '@/misc/schema'; import { awaitAll } from '@/prelude/await-all'; import { populateEmojis } from '@/misc/populate-emojis'; import { getAntennas } from '@/misc/antenna-cache'; import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const'; -export type PackedUser = SchemaType; - @EntityRepository(User) export class UserRepository extends Repository { public async getRelation(me: User['id'], target: User['id']) { @@ -164,7 +162,7 @@ export class UserRepository extends Repository { detail?: boolean, includeSecrets?: boolean, } - ): Promise { + ): Promise> { const opts = Object.assign({ detail: false, includeSecrets: false diff --git a/src/server/api/common/read-messaging-message.ts b/src/server/api/common/read-messaging-message.ts index a80a22ea99..35ff4e4693 100644 --- a/src/server/api/common/read-messaging-message.ts +++ b/src/server/api/common/read-messaging-message.ts @@ -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; diff --git a/src/server/api/endpoints/antennas/update.ts b/src/server/api/endpoints/antennas/update.ts index ff13e89bcc..d69b4feee6 100644 --- a/src/server/api/endpoints/antennas/update.ts +++ b/src/server/api/endpoints/antennas/update.ts @@ -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); }); diff --git a/src/server/api/stream/channels/antenna.ts b/src/server/api/stream/channels/antenna.ts index bf9c53c453..864e42dc66 100644 --- a/src/server/api/stream/channels/antenna.ts +++ b/src/server/api/stream/channels/antenna.ts @@ -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); } } diff --git a/src/server/api/stream/channels/channel.ts b/src/server/api/stream/channels/channel.ts index e6a9a6c696..c97a062c42 100644 --- a/src/server/api/stream/channels/channel.ts +++ b/src/server/api/stream/channels/channel.ts @@ -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; diff --git a/src/server/api/stream/channels/global-timeline.ts b/src/server/api/stream/channels/global-timeline.ts index 384ed61409..f5983ab472 100644 --- a/src/server/api/stream/channels/global-timeline.ts +++ b/src/server/api/stream/channels/global-timeline.ts @@ -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; diff --git a/src/server/api/stream/channels/hashtag.ts b/src/server/api/stream/channels/hashtag.ts index 997ab75f6d..281be4f2eb 100644 --- a/src/server/api/stream/channels/hashtag.ts +++ b/src/server/api/stream/channels/hashtag.ts @@ -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; diff --git a/src/server/api/stream/channels/home-timeline.ts b/src/server/api/stream/channels/home-timeline.ts index 0e21ab552e..52e9aec250 100644 --- a/src/server/api/stream/channels/home-timeline.ts +++ b/src/server/api/stream/channels/home-timeline.ts @@ -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 { diff --git a/src/server/api/stream/channels/hybrid-timeline.ts b/src/server/api/stream/channels/hybrid-timeline.ts index 0b28ff616b..51f95fc0cd 100644 --- a/src/server/api/stream/channels/hybrid-timeline.ts +++ b/src/server/api/stream/channels/hybrid-timeline.ts @@ -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'>) { // チャンネルの投稿ではなく、自分自身の投稿 または // チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または // チャンネルの投稿ではなく、全体公開のローカルの投稿 または diff --git a/src/server/api/stream/channels/local-timeline.ts b/src/server/api/stream/channels/local-timeline.ts index 20061410c4..a6166c2be2 100644 --- a/src/server/api/stream/channels/local-timeline.ts +++ b/src/server/api/stream/channels/local-timeline.ts @@ -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; diff --git a/src/server/api/stream/channels/main.ts b/src/server/api/stream/channels/main.ts index b99cb931da..131ac30472 100644 --- a/src/server/api/stream/channels/main.ts +++ b/src/server/api/stream/channels/main.ts @@ -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); }); } } diff --git a/src/server/api/stream/channels/messaging.ts b/src/server/api/stream/channels/messaging.ts index 015b0a7650..a75181d6d7 100644 --- a/src/server/api/stream/channels/messaging.ts +++ b/src/server/api/stream/channels/messaging.ts @@ -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 = {}; private emitTypersIntervalId: ReturnType; @@ -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; diff --git a/src/server/api/stream/channels/user-list.ts b/src/server/api/stream/channels/user-list.ts index 0ca83cd658..63b254605b 100644 --- a/src/server/api/stream/channels/user-list.ts +++ b/src/server/api/stream/channels/user-list.ts @@ -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)) { diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts index f83bc9331e..93d56c0ac6 100644 --- a/src/server/api/stream/index.ts +++ b/src/server/api/stream/index.ts @@ -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 = 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, diff --git a/src/server/api/stream/types.ts b/src/server/api/stream/types.ts new file mode 100644 index 0000000000..c58a627eb3 --- /dev/null +++ b/src/server/api/stream/types.ts @@ -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 = { [K in keyof T]: { type: K; body: T[K]; } }; +type EventUnionFromDictionary< + T extends object, + U = Events +> = U[keyof U]; + +// name/messages(spec) pairs dictionary +export type StreamMessages = { + internal: { + name: 'internal'; + spec: EventUnionFromDictionary; + }; + broadcast: { + name: 'broadcast'; + spec: EventUnionFromDictionary; + }; + user: { + name: `user:${User['id']}`; + spec: EventUnionFromDictionary; + }; + main: { + name: `mainStream:${User['id']}`; + spec: EventUnionFromDictionary; + }; + drive: { + name: `driveStream:${User['id']}`; + spec: EventUnionFromDictionary; + }; + note: { + name: `noteStream:${Note['id']}`; + spec: EventUnionFromDictionary; + }; + channel: { + name: `channelStream:${Channel['id']}`; + spec: EventUnionFromDictionary; + }; + userList: { + name: `userListStream:${UserList['id']}`; + spec: EventUnionFromDictionary; + }; + antenna: { + name: `antennaStream:${Antenna['id']}`; + spec: EventUnionFromDictionary; + }; + messaging: { + name: `messagingStream:${User['id']}-${User['id']}`; + spec: EventUnionFromDictionary; + }; + groupMessaging: { + name: `messagingStream:${UserGroup['id']}`; + spec: EventUnionFromDictionary; + }; + messagingIndex: { + name: `messagingIndexStream:${User['id']}`; + spec: EventUnionFromDictionary; + }; + reversi: { + name: `reversiStream:${User['id']}`; + spec: EventUnionFromDictionary; + }; + reversiGame: { + name: `reversiGameStream:${ReversiGame['id']}`; + spec: EventUnionFromDictionary; + }; + admin: { + name: `adminStream:${User['id']}`; + spec: EventUnionFromDictionary; + }; + notes: { + name: 'notesStream'; + spec: Packed<'Note'>; + }; +}; + +// API event definitions +// ストリームごとのEmitterの辞書を用意 +type EventEmitterDictionary = { [x in keyof StreamMessages]: Emitter void }> }; +// 共用体型を交差型にする型 https://stackoverflow.com/questions/54938141/typescript-convert-union-to-intersection +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never; +// Emitter辞書から共用体型を作り、UnionToIntersectionで交差型にする +export type StreamEventEmitter = UnionToIntersection; +// { [y in name]: (e: spec) => void }をまとめてその交差型をEmitterにかけるとts(2590)にひっかかる + +// provide stream channels union +export type StreamChannels = StreamMessages[keyof StreamMessages]['name']; diff --git a/src/services/note/read.ts b/src/services/note/read.ts index 2e221d553a..f25f86da9c 100644 --- a/src/services/note/read.ts +++ b/src/services/note/read.ts @@ -6,15 +6,15 @@ import { Not, IsNull, In } from 'typeorm'; import { Channel } from '@/models/entities/channel'; import { checkHitAntenna } from '@/misc/check-hit-antenna'; import { getAntennas } from '@/misc/antenna-cache'; -import { PackedNote } from '@/models/repositories/note'; import { readNotificationByQuery } from '@/server/api/common/read-notification'; +import { Packed } from '@/misc/schema'; /** * Mark notes as read */ export default async function( userId: User['id'], - notes: (Note | PackedNote)[], + notes: (Note | Packed<'Note'>)[], info?: { following: Set; followingChannels: Set; @@ -34,10 +34,10 @@ export default async function( })).map(x => x.followeeId)); const myAntennas = (await getAntennas()).filter(a => a.userId === userId); - const readMentions: (Note | PackedNote)[] = []; - const readSpecifiedNotes: (Note | PackedNote)[] = []; - const readChannelNotes: (Note | PackedNote)[] = []; - const readAntennaNotes: (Note | PackedNote)[] = []; + const readMentions: (Note | Packed<'Note'>)[] = []; + const readSpecifiedNotes: (Note | Packed<'Note'>)[] = []; + const readChannelNotes: (Note | Packed<'Note'>)[] = []; + const readAntennaNotes: (Note | Packed<'Note'>)[] = []; for (const note of notes) { if (note.mentions && note.mentions.includes(userId)) { @@ -52,7 +52,7 @@ export default async function( if (note.user != null) { // たぶんnullになることは無いはずだけど一応 for (const antenna of myAntennas) { - if (checkHitAntenna(antenna, note, note.user as any, undefined, Array.from(following))) { + if (await checkHitAntenna(antenna, note, note.user as any, undefined, Array.from(following))) { readAntennaNotes.push(note); } } diff --git a/src/services/push-notification.ts b/src/services/push-notification.ts index dd1c32f458..de58a839df 100644 --- a/src/services/push-notification.ts +++ b/src/services/push-notification.ts @@ -2,13 +2,12 @@ import * as push from 'web-push'; import config from '@/config/index'; import { SwSubscriptions } from '@/models/index'; import { fetchMeta } from '@/misc/fetch-meta'; -import { PackedNotification } from '../models/repositories/notification'; -import { PackedMessagingMessage } from '../models/repositories/messaging-message'; -import { pushNotificationData } from '../types'; +import { Packed } from '@/misc/schema'; +import { pushNotificationData } from '@/types'; type pushNotificationsTypes = { - 'notification': PackedNotification; - 'unreadMessagingMessage': PackedMessagingMessage; + 'notification': Packed<'Notification'>; + 'unreadMessagingMessage': Packed<'MessagingMessage'>; 'readNotifications': { notificationIds: string[] }; 'readAllNotifications': undefined; 'readAllMessagingMessages': undefined; diff --git a/src/services/stream.ts b/src/services/stream.ts index 4db1a77395..2c308a1b54 100644 --- a/src/services/stream.ts +++ b/src/services/stream.ts @@ -7,9 +7,28 @@ import { UserGroup } from '@/models/entities/user-group'; import config from '@/config/index'; import { Antenna } from '@/models/entities/antenna'; import { Channel } from '@/models/entities/channel'; +import { + StreamChannels, + AdminStreamTypes, + AntennaStreamTypes, + BroadcastTypes, + ChannelStreamTypes, + DriveStreamTypes, + GroupMessagingStreamTypes, + InternalStreamTypes, + MainStreamTypes, + MessagingIndexStreamTypes, + MessagingStreamTypes, + NoteStreamTypes, + ReversiGameStreamTypes, + ReversiStreamTypes, + UserListStreamTypes, + UserStreamTypes +} from '@/server/api/stream/types'; +import { Packed } from '@/misc/schema'; class Publisher { - private publish = (channel: string, type: string | null, value?: any): void => { + private publish = (channel: StreamChannels, type: string | null, value?: any): void => { const message = type == null ? value : value == null ? { type: type, body: null } : { type: type, body: value }; @@ -20,70 +39,70 @@ class Publisher { })); } - public publishInternalEvent = (type: string, value?: any): void => { + public publishInternalEvent = (type: K, value?: InternalStreamTypes[K]): void => { this.publish('internal', type, typeof value === 'undefined' ? null : value); } - public publishUserEvent = (userId: User['id'], type: string, value?: any): void => { + public publishUserEvent = (userId: User['id'], type: K, value?: UserStreamTypes[K]): void => { this.publish(`user:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishBroadcastStream = (type: string, value?: any): void => { + public publishBroadcastStream = (type: K, value?: BroadcastTypes[K]): void => { this.publish('broadcast', type, typeof value === 'undefined' ? null : value); } - public publishMainStream = (userId: User['id'], type: string, value?: any): void => { + public publishMainStream = (userId: User['id'], type: K, value?: MainStreamTypes[K]): void => { this.publish(`mainStream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishDriveStream = (userId: User['id'], type: string, value?: any): void => { + public publishDriveStream = (userId: User['id'], type: K, value?: DriveStreamTypes[K]): void => { this.publish(`driveStream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishNoteStream = (noteId: Note['id'], type: string, value: any): void => { + public publishNoteStream = (noteId: Note['id'], type: K, value?: NoteStreamTypes[K]): void => { this.publish(`noteStream:${noteId}`, type, { id: noteId, body: value }); } - public publishChannelStream = (channelId: Channel['id'], type: string, value?: any): void => { + public publishChannelStream = (channelId: Channel['id'], type: K, value?: ChannelStreamTypes[K]): void => { this.publish(`channelStream:${channelId}`, type, typeof value === 'undefined' ? null : value); } - public publishUserListStream = (listId: UserList['id'], type: string, value?: any): void => { + public publishUserListStream = (listId: UserList['id'], type: K, value?: UserListStreamTypes[K]): void => { this.publish(`userListStream:${listId}`, type, typeof value === 'undefined' ? null : value); } - public publishAntennaStream = (antennaId: Antenna['id'], type: string, value?: any): void => { + public publishAntennaStream = (antennaId: Antenna['id'], type: K, value?: AntennaStreamTypes[K]): void => { this.publish(`antennaStream:${antennaId}`, type, typeof value === 'undefined' ? null : value); } - public publishMessagingStream = (userId: User['id'], otherpartyId: User['id'], type: string, value?: any): void => { + public publishMessagingStream = (userId: User['id'], otherpartyId: User['id'], type: K, value?: MessagingStreamTypes[K]): void => { this.publish(`messagingStream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); } - public publishGroupMessagingStream = (groupId: UserGroup['id'], type: string, value?: any): void => { + public publishGroupMessagingStream = (groupId: UserGroup['id'], type: K, value?: GroupMessagingStreamTypes[K]): void => { this.publish(`messagingStream:${groupId}`, type, typeof value === 'undefined' ? null : value); } - public publishMessagingIndexStream = (userId: User['id'], type: string, value?: any): void => { + public publishMessagingIndexStream = (userId: User['id'], type: K, value?: MessagingIndexStreamTypes[K]): void => { this.publish(`messagingIndexStream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishReversiStream = (userId: User['id'], type: string, value?: any): void => { + public publishReversiStream = (userId: User['id'], type: K, value?: ReversiStreamTypes[K]): void => { this.publish(`reversiStream:${userId}`, type, typeof value === 'undefined' ? null : value); } - public publishReversiGameStream = (gameId: ReversiGame['id'], type: string, value?: any): void => { + public publishReversiGameStream = (gameId: ReversiGame['id'], type: K, value?: ReversiGameStreamTypes[K]): void => { this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value); } - public publishNotesStream = (note: any): void => { + public publishNotesStream = (note: Packed<'Note'>): void => { this.publish('notesStream', null, note); } - public publishAdminStream = (userId: User['id'], type: string, value?: any): void => { + public publishAdminStream = (userId: User['id'], type: K, value?: AdminStreamTypes[K]): void => { this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value); } } diff --git a/yarn.lock b/yarn.lock index 08563ec3c9..5485140fc2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10279,6 +10279,11 @@ streamsearch@0.1.2: resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= +strict-event-emitter-types@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strict-event-emitter-types/-/strict-event-emitter-types-2.0.0.tgz#05e15549cb4da1694478a53543e4e2f4abcf277f" + integrity sha512-Nk/brWYpD85WlOgzw5h173aci0Teyv8YdIAEtV+N88nDB0dLlazZyJMIsN6eo1/AR61l+p6CJTG1JIyFaoNEEA== + strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"