diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2018-07-11 14:06:07 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-07-11 14:06:07 +0900 |
| commit | 58fdeb50f34ff9f66cf201583924911c2ac65205 (patch) | |
| tree | d462b3a20d79c6ed7e15e7961aacfe54cb4265a9 /src | |
| parent | oops (diff) | |
| parent | wip (diff) | |
| download | misskey-58fdeb50f34ff9f66cf201583924911c2ac65205.tar.gz misskey-58fdeb50f34ff9f66cf201583924911c2ac65205.tar.bz2 misskey-58fdeb50f34ff9f66cf201583924911c2ac65205.zip | |
Merge pull request #1879 from syuilo/hybrid-tl
Hybrid tl
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/app/common/scripts/streaming/hybrid-timeline.ts | 34 | ||||
| -rw-r--r-- | src/client/app/desktop/views/components/timeline.core.vue | 22 | ||||
| -rw-r--r-- | src/client/app/desktop/views/components/timeline.vue | 2 | ||||
| -rw-r--r-- | src/client/app/desktop/views/pages/deck/deck.tl-column.vue | 2 | ||||
| -rw-r--r-- | src/client/app/desktop/views/pages/deck/deck.tl.vue | 34 | ||||
| -rw-r--r-- | src/client/app/desktop/views/pages/deck/deck.vue | 9 | ||||
| -rw-r--r-- | src/client/app/mios.ts | 4 | ||||
| -rw-r--r-- | src/client/app/mobile/views/pages/home.timeline.vue | 22 | ||||
| -rw-r--r-- | src/client/app/mobile/views/pages/home.vue | 3 | ||||
| -rw-r--r-- | src/server/api/call.ts | 2 | ||||
| -rw-r--r-- | src/server/api/common/get-friends.ts | 12 | ||||
| -rw-r--r-- | src/server/api/endpoints.ts | 7 | ||||
| -rw-r--r-- | src/server/api/endpoints/notes/hybrid-timeline.ts | 216 | ||||
| -rw-r--r-- | src/server/api/stream/hybrid-timeline.ts | 47 | ||||
| -rw-r--r-- | src/services/note/create.ts | 9 | ||||
| -rw-r--r-- | src/stream.ts | 5 |
16 files changed, 387 insertions, 43 deletions
diff --git a/src/client/app/common/scripts/streaming/hybrid-timeline.ts b/src/client/app/common/scripts/streaming/hybrid-timeline.ts new file mode 100644 index 0000000000..cd290797c4 --- /dev/null +++ b/src/client/app/common/scripts/streaming/hybrid-timeline.ts @@ -0,0 +1,34 @@ +import Stream from './stream'; +import StreamManager from './stream-manager'; +import MiOS from '../../../mios'; + +/** + * Hybrid timeline stream connection + */ +export class HybridTimelineStream extends Stream { + constructor(os: MiOS, me) { + super(os, 'hybrid-timeline', { + i: me.token + }); + } +} + +export class HybridTimelineStreamManager extends StreamManager<HybridTimelineStream> { + private me; + private os: MiOS; + + constructor(os: MiOS, me) { + super(); + + this.me = me; + this.os = os; + } + + public getConnection() { + if (this.connection == null) { + this.connection = new HybridTimelineStream(this.os, this.me); + } + + return this.connection; + } +} diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue index 1728dad286..15e188be67 100644 --- a/src/client/app/desktop/views/components/timeline.core.vue +++ b/src/client/app/desktop/views/components/timeline.core.vue @@ -43,19 +43,21 @@ export default Vue.extend({ }, stream(): any { - return this.src == 'home' - ? (this as any).os.stream - : this.src == 'local' - ? (this as any).os.streams.localTimelineStream - : (this as any).os.streams.globalTimelineStream; + switch (this.src) { + case 'home': return (this as any).os.stream; + case 'local': return (this as any).os.streams.localTimelineStream; + case 'hybrid': return (this as any).os.streams.hybridTimelineStream; + case 'global': return (this as any).os.streams.globalTimelineStream; + } }, endpoint(): string { - return this.src == 'home' - ? 'notes/timeline' - : this.src == 'local' - ? 'notes/local-timeline' - : 'notes/global-timeline'; + switch (this.src) { + case 'home': return 'notes/timeline'; + case 'local': return 'notes/local-timeline'; + case 'hybrid': return 'notes/hybrid-timeline'; + case 'global': return 'notes/global-timeline'; + } }, canFetchMore(): boolean { diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue index 0728b78aa9..a5ea1487ab 100644 --- a/src/client/app/desktop/views/components/timeline.vue +++ b/src/client/app/desktop/views/components/timeline.vue @@ -3,12 +3,14 @@ <header> <span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span> <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% %i18n:@local%</span> + <span :data-active="src == 'hybrid'" @click="src = 'hybrid'">%fa:share-alt% %i18n:@hybrid%</span> <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span> <span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span> <button @click="chooseList" title="%i18n:@list%">%fa:list%</button> </header> <x-core v-if="src == 'home'" ref="tl" key="home" src="home"/> <x-core v-if="src == 'local'" ref="tl" key="local" src="local"/> + <x-core v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> <x-core v-if="src == 'global'" ref="tl" key="global" src="global"/> <mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/> </div> diff --git a/src/client/app/desktop/views/pages/deck/deck.tl-column.vue b/src/client/app/desktop/views/pages/deck/deck.tl-column.vue index ffe1da670b..231b505f5d 100644 --- a/src/client/app/desktop/views/pages/deck/deck.tl-column.vue +++ b/src/client/app/desktop/views/pages/deck/deck.tl-column.vue @@ -3,6 +3,7 @@ <span slot="header"> <template v-if="column.type == 'home'">%fa:home%</template> <template v-if="column.type == 'local'">%fa:R comments%</template> + <template v-if="column.type == 'hybrid'">%fa:share-alt%</template> <template v-if="column.type == 'global'">%fa:globe%</template> <template v-if="column.type == 'list'">%fa:list%</template> <span>{{ name }}</span> @@ -61,6 +62,7 @@ export default Vue.extend({ switch (this.column.type) { case 'home': return '%i18n:common.deck.home%'; case 'local': return '%i18n:common.deck.local%'; + case 'hybrid': return '%i18n:common.deck.hybrid%'; case 'global': return '%i18n:common.deck.global%'; case 'list': return this.column.list.title; } diff --git a/src/client/app/desktop/views/pages/deck/deck.tl.vue b/src/client/app/desktop/views/pages/deck/deck.tl.vue index 8e05f09c5d..d402ee6a8b 100644 --- a/src/client/app/desktop/views/pages/deck/deck.tl.vue +++ b/src/client/app/desktop/views/pages/deck/deck.tl.vue @@ -41,27 +41,29 @@ export default Vue.extend({ }; }, - watch: { - mediaOnly() { - this.fetch(); - } - }, - computed: { stream(): any { - return this.src == 'home' - ? (this as any).os.stream - : this.src == 'local' - ? (this as any).os.streams.localTimelineStream - : (this as any).os.streams.globalTimelineStream; + switch (this.src) { + case 'home': return (this as any).os.stream; + case 'local': return (this as any).os.streams.localTimelineStream; + case 'hybrid': return (this as any).os.streams.hybridTimelineStream; + case 'global': return (this as any).os.streams.globalTimelineStream; + } }, endpoint(): string { - return this.src == 'home' - ? 'notes/timeline' - : this.src == 'local' - ? 'notes/local-timeline' - : 'notes/global-timeline'; + switch (this.src) { + case 'home': return 'notes/timeline'; + case 'local': return 'notes/local-timeline'; + case 'hybrid': return 'notes/hybrid-timeline'; + case 'global': return 'notes/global-timeline'; + } + }, + }, + + watch: { + mediaOnly() { + this.fetch(); } }, diff --git a/src/client/app/desktop/views/pages/deck/deck.vue b/src/client/app/desktop/views/pages/deck/deck.vue index da4acb8cca..26b989656e 100644 --- a/src/client/app/desktop/views/pages/deck/deck.vue +++ b/src/client/app/desktop/views/pages/deck/deck.vue @@ -120,6 +120,15 @@ export default Vue.extend({ }); } }, { + icon: '%fa:share-alt%', + text: '%i18n:common.deck.hybrid%', + action: () => { + this.$store.dispatch('settings/addDeckColumn', { + id: uuid(), + type: 'hybrid' + }); + } + }, { icon: '%fa:globe%', text: '%i18n:common.deck.global%', action: () => { diff --git a/src/client/app/mios.ts b/src/client/app/mios.ts index ed41e7e433..565c8bf1f5 100644 --- a/src/client/app/mios.ts +++ b/src/client/app/mios.ts @@ -15,6 +15,7 @@ import { ReversiStreamManager } from './common/scripts/streaming/games/reversi/r import Err from './common/views/components/connect-failed.vue'; import { LocalTimelineStreamManager } from './common/scripts/streaming/local-timeline'; +import { HybridTimelineStreamManager } from './common/scripts/streaming/hybrid-timeline'; import { GlobalTimelineStreamManager } from './common/scripts/streaming/global-timeline'; //#region api requests @@ -103,6 +104,7 @@ export default class MiOS extends EventEmitter { */ public streams: { localTimelineStream: LocalTimelineStreamManager; + hybridTimelineStream: HybridTimelineStreamManager; globalTimelineStream: GlobalTimelineStreamManager; driveStream: DriveStreamManager; serverStatsStream: ServerStatsStreamManager; @@ -111,6 +113,7 @@ export default class MiOS extends EventEmitter { reversiStream: ReversiStreamManager; } = { localTimelineStream: null, + hybridTimelineStream: null, globalTimelineStream: null, driveStream: null, serverStatsStream: null, @@ -230,6 +233,7 @@ export default class MiOS extends EventEmitter { // Init other stream manager this.streams.localTimelineStream = new LocalTimelineStreamManager(this, this.store.state.i); + this.streams.hybridTimelineStream = new HybridTimelineStreamManager(this, this.store.state.i); this.streams.globalTimelineStream = new GlobalTimelineStreamManager(this, this.store.state.i); this.streams.driveStream = new DriveStreamManager(this, this.store.state.i); this.streams.messagingIndexStream = new MessagingIndexStreamManager(this, this.store.state.i); diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue index 364367b940..93d1364e09 100644 --- a/src/client/app/mobile/views/pages/home.timeline.vue +++ b/src/client/app/mobile/views/pages/home.timeline.vue @@ -42,19 +42,21 @@ export default Vue.extend({ }, stream(): any { - return this.src == 'home' - ? (this as any).os.stream - : this.src == 'local' - ? (this as any).os.streams.localTimelineStream - : (this as any).os.streams.globalTimelineStream; + switch (this.src) { + case 'home': return (this as any).os.stream; + case 'local': return (this as any).os.streams.localTimelineStream; + case 'hybrid': return (this as any).os.streams.hybridTimelineStream; + case 'global': return (this as any).os.streams.globalTimelineStream; + } }, endpoint(): string { - return this.src == 'home' - ? 'notes/timeline' - : this.src == 'local' - ? 'notes/local-timeline' - : 'notes/global-timeline'; + switch (this.src) { + case 'home': return 'notes/timeline'; + case 'local': return 'notes/local-timeline'; + case 'hybrid': return 'notes/hybrid-timeline'; + case 'global': return 'notes/global-timeline'; + } }, canFetchMore(): boolean { diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue index c0c2ee8ab5..09930aca5a 100644 --- a/src/client/app/mobile/views/pages/home.vue +++ b/src/client/app/mobile/views/pages/home.vue @@ -4,6 +4,7 @@ <span> <span v-if="src == 'home'">%fa:home%%i18n:@home%</span> <span v-if="src == 'local'">%fa:R comments%%i18n:@local%</span> + <span v-if="src == 'hybrid'">%fa:share-alt%%i18n:@hybrid%</span> <span v-if="src == 'global'">%fa:globe%%i18n:@global%</span> <span v-if="src == 'list'">%fa:list%{{ list.title }}</span> </span> @@ -24,6 +25,7 @@ <div> <span :data-active="src == 'home'" @click="src = 'home'">%fa:home% %i18n:@home%</span> <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% %i18n:@local%</span> + <span :data-active="src == 'hybrid'" @click="src = 'hybrid'">%fa:share-alt% %i18n:@hybrid%</span> <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% %i18n:@global%</span> <template v-if="lists"> <span v-for="l in lists" :data-active="src == 'list' && list == l" @click="src = 'list'; list = l" :key="l.id">%fa:list% {{ l.title }}</span> @@ -35,6 +37,7 @@ <div class="tl"> <x-tl v-if="src == 'home'" ref="tl" key="home" src="home"/> <x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/> + <x-tl v-if="src == 'hybrid'" ref="tl" key="hybrid" src="hybrid"/> <x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/> <mk-user-list-timeline v-if="src == 'list'" ref="tl" :key="list.id" :list="list"/> </div> diff --git a/src/server/api/call.ts b/src/server/api/call.ts index 9257ed590e..2b00f5df0a 100644 --- a/src/server/api/call.ts +++ b/src/server/api/call.ts @@ -8,7 +8,7 @@ export default (endpoint: string | Endpoint, user: IUser, app: IApp, data: any, const ep = typeof endpoint == 'string' ? endpoints.find(e => e.name == endpoint) : endpoint; - if (ep.name.includes('.') { + if (ep.name.includes('.')) { return rej('INVALID_ENDPOINT'); } diff --git a/src/server/api/common/get-friends.ts b/src/server/api/common/get-friends.ts index 50ba71ea96..e0b05f73df 100644 --- a/src/server/api/common/get-friends.ts +++ b/src/server/api/common/get-friends.ts @@ -23,12 +23,16 @@ export const getFriendIds = async (me: mongodb.ObjectID, includeMe = true) => { return myfollowingIds; }; -export const getFriends = async (me: mongodb.ObjectID, includeMe = true) => { +export const getFriends = async (me: mongodb.ObjectID, includeMe = true, remoteOnly = false) => { + const q: any = remoteOnly ? { + followerId: me, + '_followee.host': { $ne: null } + } : { + followerId: me + }; // Fetch relation to other users who the I follows const followings = await Following - .find({ - followerId: me - }); + .find(q); // ID list of other users who the I follows const myfollowings = followings.map(following => ({ diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts index 16237583bf..58b251c924 100644 --- a/src/server/api/endpoints.ts +++ b/src/server/api/endpoints.ts @@ -544,6 +544,13 @@ const endpoints: Endpoint[] = [ } }, { + name: 'notes/hybrid-timeline', + limit: { + duration: ms('10minutes'), + max: 100 + } + }, + { name: 'notes/global-timeline', limit: { duration: ms('10minutes'), diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts new file mode 100644 index 0000000000..c9b352fe8e --- /dev/null +++ b/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -0,0 +1,216 @@ +import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; +import Note from '../../../../models/note'; +import Mute from '../../../../models/mute'; +import { getFriends } from '../../common/get-friends'; +import { pack } from '../../../../models/note'; +import { ILocalUser } from '../../../../models/user'; +import getParams from '../../get-params'; + +export const meta = { + name: 'notes/hybrid-timeline', + + desc: { + ja: 'ハイブリッドタイムラインを取得します。' + }, + + params: { + limit: $.num.optional.range(1, 100).note({ + default: 10, + desc: { + ja: '最大数' + } + }), + + sinceId: $.type(ID).optional.note({ + desc: { + ja: '指定すると、この投稿を基点としてより新しい投稿を取得します' + } + }), + + untilId: $.type(ID).optional.note({ + desc: { + ja: '指定すると、この投稿を基点としてより古い投稿を取得します' + } + }), + + sinceDate: $.num.optional.note({ + desc: { + ja: '指定した時間を基点としてより新しい投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。' + } + }), + + untilDate: $.num.optional.note({ + desc: { + ja: '指定した時間を基点としてより古い投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。' + } + }), + + includeMyRenotes: $.bool.optional.note({ + default: true, + desc: { + ja: '自分の行ったRenoteを含めるかどうか' + } + }), + + includeRenotedMyNotes: $.bool.optional.note({ + default: true, + desc: { + ja: 'Renoteされた自分の投稿を含めるかどうか' + } + }), + + mediaOnly: $.bool.optional.note({ + desc: { + ja: 'true にすると、メディアが添付された投稿だけ取得します' + } + }), + } +}; + +/** + * Get hybrid timeline of myself + */ +export default async (params: any, user: ILocalUser) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) throw psErr; + + // Check if only one of sinceId, untilId, sinceDate, untilDate specified + if ([ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate].filter(x => x != null).length > 1) { + throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; + } + + const [followings, mutedUserIds] = await Promise.all([ + // フォローを取得 + // Fetch following + getFriends(user._id, true, true), + + // ミュートしているユーザーを取得 + Mute.find({ + muterId: user._id + }).then(ms => ms.map(m => m.muteeId)) + ]); + + //#region Construct query + const sort = { + _id: -1 + }; + + const followQuery = followings.map(f => f.stalk ? { + userId: f.id + } : { + userId: f.id, + + // ストーキングしてないならリプライは含めない(ただし投稿者自身の投稿へのリプライ、自分の投稿へのリプライ、自分のリプライは含める) + $or: [{ + // リプライでない + replyId: null + }, { // または + // リプライだが返信先が投稿者自身の投稿 + $expr: { + $eq: ['$_reply.userId', '$userId'] + } + }, { // または + // リプライだが返信先が自分(フォロワー)の投稿 + '_reply.userId': user._id + }, { // または + // 自分(フォロワー)が送信したリプライ + userId: user._id + }] + }); + + const query = { + $and: [{ + $or: [{ + // フォローしている人の投稿 + $or: followQuery + }, { + // local + '_user.host': null + }], + + // mute + userId: { + $nin: mutedUserIds + }, + '_reply.userId': { + $nin: mutedUserIds + }, + '_renote.userId': { + $nin: mutedUserIds + }, + }] + } as any; + + // MongoDBではトップレベルで否定ができないため、De Morganの法則を利用してクエリします。 + // つまり、「『自分の投稿かつRenote』ではない」を「『自分の投稿ではない』または『Renoteではない』」と表現します。 + // for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws + + if (ps.includeMyRenotes === false) { + query.$and.push({ + $or: [{ + userId: { $ne: user._id } + }, { + renoteId: null + }, { + text: { $ne: null } + }, { + mediaIds: { $ne: [] } + }, { + poll: { $ne: null } + }] + }); + } + + if (ps.includeRenotedMyNotes === false) { + query.$and.push({ + $or: [{ + '_renote.userId': { $ne: user._id } + }, { + renoteId: null + }, { + text: { $ne: null } + }, { + mediaIds: { $ne: [] } + }, { + poll: { $ne: null } + }] + }); + } + + if (ps.mediaOnly) { + query.$and.push({ + mediaIds: { $exists: true, $ne: [] } + }); + } + + if (ps.sinceId) { + sort._id = 1; + query._id = { + $gt: ps.sinceId + }; + } else if (ps.untilId) { + query._id = { + $lt: ps.untilId + }; + } else if (ps.sinceDate) { + sort._id = 1; + query.createdAt = { + $gt: new Date(ps.sinceDate) + }; + } else if (ps.untilDate) { + query.createdAt = { + $lt: new Date(ps.untilDate) + }; + } + //#endregion + + // Issue query + const timeline = await Note + .find(query, { + limit: ps.limit, + sort: sort + }); + + // Serialize + return await Promise.all(timeline.map(note => pack(note, user))); +}; diff --git a/src/server/api/stream/hybrid-timeline.ts b/src/server/api/stream/hybrid-timeline.ts new file mode 100644 index 0000000000..55f9fbb788 --- /dev/null +++ b/src/server/api/stream/hybrid-timeline.ts @@ -0,0 +1,47 @@ +import * as websocket from 'websocket'; +import * as redis from 'redis'; + +import { IUser } from '../../../models/user'; +import Mute from '../../../models/mute'; +import { pack } from '../../../models/note'; + +export default async function( + request: websocket.request, + connection: websocket.connection, + subscriber: redis.RedisClient, + user: IUser +) { + // Subscribe stream + subscriber.subscribe(`misskey:hybrid-timeline:${user._id}`); + + const mute = await Mute.find({ muterId: user._id }); + const mutedUserIds = mute.map(m => m.muteeId.toString()); + + subscriber.on('message', async (_, data) => { + const note = JSON.parse(data); + + //#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (mutedUserIds.indexOf(note.userId) != -1) { + return; + } + if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) { + return; + } + if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) { + return; + } + //#endregion + + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await pack(note.renoteId, user, { + detail: true + }); + } + + connection.send(JSON.stringify({ + type: 'note', + body: note + })); + }); +} diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 25281df01a..cc7474292f 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -1,7 +1,7 @@ import es from '../../db/elasticsearch'; import Note, { pack, INote } from '../../models/note'; import User, { isLocalUser, IUser, isRemoteUser, IRemoteUser, ILocalUser } from '../../models/user'; -import stream, { publishLocalTimelineStream, publishGlobalTimelineStream, publishUserListStream } from '../../stream'; +import stream, { publishLocalTimelineStream, publishHybridTimelineStream, publishGlobalTimelineStream, publishUserListStream } from '../../stream'; import Following from '../../models/following'; import { deliver } from '../../queue'; import renderNote from '../../remote/activitypub/renderer/note'; @@ -266,9 +266,10 @@ export default async (user: IUser, data: { // Publish event to myself's stream stream(note.userId, 'note', noteObj); - // Publish note to local timeline stream + // Publish note to local and hybrid timeline stream if (note.visibility != 'home') { publishLocalTimelineStream(noteObj); + publishHybridTimelineStream(noteObj); } } } @@ -303,6 +304,10 @@ export default async (user: IUser, data: { // Publish event to followers stream stream(following.followerId, 'note', noteObj); + + if (isRemoteUser(user)) { + publishHybridTimelineStream(following.followerId, noteObj); + } } else { //#region AP配送 // フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信 diff --git a/src/stream.ts b/src/stream.ts index da2f9aecd7..b2aa4f0e2a 100644 --- a/src/stream.ts +++ b/src/stream.ts @@ -49,6 +49,10 @@ class MisskeyEvent { this.redisClient.publish('misskey:local-timeline', JSON.stringify(note)); } + public publishHybridTimelineStream(userId: ID, note: any): void { + this.redisClient.publish(`misskey:hybrid-timeline:${userId}`, JSON.stringify(note)); + } + public publishGlobalTimelineStream(note: any): void { this.redisClient.publish('misskey:global-timeline', JSON.stringify(note)); } @@ -67,6 +71,7 @@ const ev = new MisskeyEvent(); export default ev.publishUserStream.bind(ev); export const publishLocalTimelineStream = ev.publishLocalTimelineStream.bind(ev); +export const publishHybridTimelineStream = ev.publishHybridTimelineStream.bind(ev); export const publishGlobalTimelineStream = ev.publishGlobalTimelineStream.bind(ev); export const publishDriveStream = ev.publishDriveStream.bind(ev); export const publishUserListStream = ev.publishUserListStream.bind(ev); |