diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2020-08-18 22:44:21 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-08-18 22:44:21 +0900 |
| commit | 9855405b8989713b81709fc1700e2ead97423467 (patch) | |
| tree | 54254d2159378d1903e962f0fb37c799bb0f4464 /src/server/api | |
| parent | Sign (request-target) Fix #6652 (#6656) (diff) | |
| download | sharkey-9855405b8989713b81709fc1700e2ead97423467.tar.gz sharkey-9855405b8989713b81709fc1700e2ead97423467.tar.bz2 sharkey-9855405b8989713b81709fc1700e2ead97423467.zip | |
Channel (#6621)
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wop
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* add notes
* wip
* wip
* wip
* wip
* sound
* wip
* add kick_gaba2
* wip
Diffstat (limited to 'src/server/api')
25 files changed, 624 insertions, 12 deletions
diff --git a/src/server/api/common/generate-channel-query.ts b/src/server/api/common/generate-channel-query.ts new file mode 100644 index 0000000000..c0337b2c6b --- /dev/null +++ b/src/server/api/common/generate-channel-query.ts @@ -0,0 +1,24 @@ +import { User } from '../../../models/entities/user'; +import { ChannelFollowings } from '../../../models'; +import { Brackets, SelectQueryBuilder } from 'typeorm'; + +export function generateChannelQuery(q: SelectQueryBuilder<any>, me?: User | null) { + if (me == null) { + q.andWhere('note.channelId IS NULL'); + } else { + q.leftJoinAndSelect('note.channel', 'channel'); + + const channelFollowingQuery = ChannelFollowings.createQueryBuilder('channelFollowing') + .select('channelFollowing.followeeId') + .where('channelFollowing.followerId = :followerId', { followerId: me.id }); + + q.andWhere(new Brackets(qb => { qb + // チャンネルのノートではない + .where('note.channelId IS NULL') + // または自分がフォローしているチャンネルのノート + .orWhere(`note.channelId IN (${ channelFollowingQuery.getQuery() })`); + })); + + q.setParameters(channelFollowingQuery.getParameters()); + } +} diff --git a/src/server/api/endpoints/channels/create.ts b/src/server/api/endpoints/channels/create.ts new file mode 100644 index 0000000000..53436e703d --- /dev/null +++ b/src/server/api/endpoints/channels/create.ts @@ -0,0 +1,68 @@ +import $ from 'cafy'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Channels, DriveFiles } from '../../../../models'; +import { Channel } from '../../../../models/entities/channel'; +import { genId } from '../../../../misc/gen-id'; +import { ID } from '../../../../misc/cafy-id'; + +export const meta = { + tags: ['channels'], + + requireCredential: true as const, + + kind: 'write:channels', + + params: { + name: { + validator: $.str.range(1, 128) + }, + + description: { + validator: $.nullable.optional.str.range(1, 2048) + }, + + bannerId: { + validator: $.nullable.optional.type(ID), + } + }, + + res: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'Channel', + }, + + errors: { + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: 'cd1e9f3e-5a12-4ab4-96f6-5d0a2cc32050' + }, + } +}; + +export default define(meta, async (ps, user) => { + let banner = null; + if (ps.bannerId != null) { + banner = await DriveFiles.findOne({ + id: ps.bannerId, + userId: user.id + }); + + if (banner == null) { + throw new ApiError(meta.errors.noSuchFile); + } + } + + const channel = await Channels.save({ + id: genId(), + createdAt: new Date(), + userId: user.id, + name: ps.name, + description: ps.description || null, + bannerId: banner ? banner.id : null, + } as Channel); + + return await Channels.pack(channel, user); +}); diff --git a/src/server/api/endpoints/channels/featured.ts b/src/server/api/endpoints/channels/featured.ts new file mode 100644 index 0000000000..abb0a19e2d --- /dev/null +++ b/src/server/api/endpoints/channels/featured.ts @@ -0,0 +1,28 @@ +import define from '../../define'; +import { Channels } from '../../../../models'; + +export const meta = { + tags: ['channels'], + + requireCredential: false as const, + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'Channel', + } + }, +}; + +export default define(meta, async (ps, me) => { + const query = Channels.createQueryBuilder('channel') + .where('channel.lastNotedAt IS NOT NULL') + .orderBy('channel.lastNotedAt', 'DESC'); + + const channels = await query.take(10).getMany(); + + return await Promise.all(channels.map(x => Channels.pack(x, me))); +}); diff --git a/src/server/api/endpoints/channels/follow.ts b/src/server/api/endpoints/channels/follow.ts new file mode 100644 index 0000000000..bf2f2bbb57 --- /dev/null +++ b/src/server/api/endpoints/channels/follow.ts @@ -0,0 +1,45 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Channels, ChannelFollowings } from '../../../../models'; +import { genId } from '../../../../misc/gen-id'; + +export const meta = { + tags: ['channels'], + + requireCredential: true as const, + + kind: 'write:channels', + + params: { + channelId: { + validator: $.type(ID), + }, + }, + + errors: { + noSuchChannel: { + message: 'No such channel.', + code: 'NO_SUCH_CHANNEL', + id: 'c0031718-d573-4e85-928e-10039f1fbb68' + }, + } +}; + +export default define(meta, async (ps, user) => { + const channel = await Channels.findOne({ + id: ps.channelId, + }); + + if (channel == null) { + throw new ApiError(meta.errors.noSuchChannel); + } + + await ChannelFollowings.save({ + id: genId(), + createdAt: new Date(), + followerId: user.id, + followeeId: channel.id, + }); +}); diff --git a/src/server/api/endpoints/channels/followed.ts b/src/server/api/endpoints/channels/followed.ts new file mode 100644 index 0000000000..05c2ec6a75 --- /dev/null +++ b/src/server/api/endpoints/channels/followed.ts @@ -0,0 +1,28 @@ +import define from '../../define'; +import { Channels, ChannelFollowings } from '../../../../models'; + +export const meta = { + tags: ['channels', 'account'], + + requireCredential: true as const, + + kind: 'read:channels', + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'Channel', + } + }, +}; + +export default define(meta, async (ps, me) => { + const followings = await ChannelFollowings.find({ + followerId: me.id, + }); + + return await Promise.all(followings.map(x => Channels.pack(x.followeeId, me))); +}); diff --git a/src/server/api/endpoints/channels/owned.ts b/src/server/api/endpoints/channels/owned.ts new file mode 100644 index 0000000000..9e563c0ac5 --- /dev/null +++ b/src/server/api/endpoints/channels/owned.ts @@ -0,0 +1,28 @@ +import define from '../../define'; +import { Channels } from '../../../../models'; + +export const meta = { + tags: ['channels', 'account'], + + requireCredential: true as const, + + kind: 'read:channels', + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'Channel', + } + }, +}; + +export default define(meta, async (ps, me) => { + const channels = await Channels.find({ + userId: me.id, + }); + + return await Promise.all(channels.map(x => Channels.pack(x, me))); +}); diff --git a/src/server/api/endpoints/channels/pin-note.ts b/src/server/api/endpoints/channels/pin-note.ts new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/src/server/api/endpoints/channels/pin-note.ts diff --git a/src/server/api/endpoints/channels/show.ts b/src/server/api/endpoints/channels/show.ts new file mode 100644 index 0000000000..63057dd57f --- /dev/null +++ b/src/server/api/endpoints/channels/show.ts @@ -0,0 +1,43 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Channels } from '../../../../models'; + +export const meta = { + tags: ['channels'], + + requireCredential: false as const, + + params: { + channelId: { + validator: $.type(ID), + }, + }, + + res: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'Channel', + }, + + errors: { + noSuchChannel: { + message: 'No such channel.', + code: 'NO_SUCH_CHANNEL', + id: '6f6c314b-7486-4897-8966-c04a66a02923' + }, + } +}; + +export default define(meta, async (ps, me) => { + const channel = await Channels.findOne({ + id: ps.channelId, + }); + + if (channel == null) { + throw new ApiError(meta.errors.noSuchChannel); + } + + return await Channels.pack(channel, me); +}); diff --git a/src/server/api/endpoints/channels/timeline.ts b/src/server/api/endpoints/channels/timeline.ts new file mode 100644 index 0000000000..3ae28fc67a --- /dev/null +++ b/src/server/api/endpoints/channels/timeline.ts @@ -0,0 +1,99 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Notes, Channels } from '../../../../models'; +import { makePaginationQuery } from '../../common/make-pagination-query'; +import { activeUsersChart } from '../../../../services/chart'; + +export const meta = { + tags: ['notes', 'channels'], + + requireCredential: false as const, + + params: { + channelId: { + validator: $.type(ID), + desc: { + 'ja-JP': 'チャンネルのID' + } + }, + + limit: { + validator: $.optional.num.range(1, 100), + default: 10, + desc: { + 'ja-JP': '最大数' + } + }, + + sinceId: { + validator: $.optional.type(ID), + desc: { + 'ja-JP': '指定すると、その投稿を基点としてより新しい投稿を取得します' + } + }, + + untilId: { + validator: $.optional.type(ID), + desc: { + 'ja-JP': '指定すると、その投稿を基点としてより古い投稿を取得します' + } + }, + + sinceDate: { + validator: $.optional.num, + desc: { + 'ja-JP': '指定した時間を基点としてより新しい投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。' + } + }, + + untilDate: { + validator: $.optional.num, + desc: { + 'ja-JP': '指定した時間を基点としてより古い投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。' + } + }, + }, + + res: { + type: 'array' as const, + optional: false as const, nullable: false as const, + items: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'Note', + } + }, + + errors: { + noSuchChannel: { + message: 'No such channel.', + code: 'NO_SUCH_CHANNEL', + id: '4d0eeeba-a02c-4c3c-9966-ef60d38d2e7f' + } + } +}; + +export default define(meta, async (ps, user) => { + const channel = await Channels.findOne({ + id: ps.channelId, + }); + + if (channel == null) { + throw new ApiError(meta.errors.noSuchChannel); + } + + //#region Construct query + const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId) + .andWhere('note.channelId = :channelId', { channelId: channel.id }) + .leftJoinAndSelect('note.user', 'user') + .leftJoinAndSelect('note.channel', 'channel'); + //#endregion + + const timeline = await query.take(ps.limit!).getMany(); + + activeUsersChart.update(user); + + return await Notes.packMany(timeline, user); +}); diff --git a/src/server/api/endpoints/channels/unfollow.ts b/src/server/api/endpoints/channels/unfollow.ts new file mode 100644 index 0000000000..8cab5c36a6 --- /dev/null +++ b/src/server/api/endpoints/channels/unfollow.ts @@ -0,0 +1,42 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Channels, ChannelFollowings } from '../../../../models'; + +export const meta = { + tags: ['channels'], + + requireCredential: true as const, + + kind: 'write:channels', + + params: { + channelId: { + validator: $.type(ID), + }, + }, + + errors: { + noSuchChannel: { + message: 'No such channel.', + code: 'NO_SUCH_CHANNEL', + id: '19959ee9-0153-4c51-bbd9-a98c49dc59d6' + }, + } +}; + +export default define(meta, async (ps, user) => { + const channel = await Channels.findOne({ + id: ps.channelId, + }); + + if (channel == null) { + throw new ApiError(meta.errors.noSuchChannel); + } + + await ChannelFollowings.delete({ + followerId: user.id, + followeeId: channel.id, + }); +}); diff --git a/src/server/api/endpoints/channels/update.ts b/src/server/api/endpoints/channels/update.ts new file mode 100644 index 0000000000..8b94646ad1 --- /dev/null +++ b/src/server/api/endpoints/channels/update.ts @@ -0,0 +1,93 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ApiError } from '../../error'; +import { Channels, DriveFiles } from '../../../../models'; + +export const meta = { + tags: ['channels'], + + requireCredential: true as const, + + kind: 'write:channels', + + params: { + channelId: { + validator: $.type(ID), + }, + + name: { + validator: $.optional.str.range(1, 128) + }, + + description: { + validator: $.nullable.optional.str.range(1, 2048) + }, + + bannerId: { + validator: $.nullable.optional.type(ID), + } + }, + + res: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'Channel', + }, + + errors: { + noSuchChannel: { + message: 'No such channel.', + code: 'NO_SUCH_CHANNEL', + id: 'f9c5467f-d492-4c3c-9a8d-a70dacc86512' + }, + + accessDenied: { + message: 'You do not have edit privilege of the channel.', + code: 'ACCESS_DENIED', + id: '1fb7cb09-d46a-4fdf-b8df-057788cce513' + }, + + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: 'e86c14a4-0da2-4032-8df3-e737a04c7f3b' + }, + } +}; + +export default define(meta, async (ps, me) => { + const channel = await Channels.findOne({ + id: ps.channelId, + }); + + if (channel == null) { + throw new ApiError(meta.errors.noSuchChannel); + } + + if (channel.userId !== me.id) { + throw new ApiError(meta.errors.accessDenied); + } + + let banner = undefined; + if (ps.bannerId != null) { + banner = await DriveFiles.findOne({ + id: ps.bannerId, + userId: me.id + }); + + if (banner == null) { + throw new ApiError(meta.errors.noSuchFile); + } + } else if (ps.bannerId === null) { + banner = null; + } + + await Channels.update(channel.id, { + ...(ps.name !== undefined ? { name: ps.name } : {}), + ...(ps.description !== undefined ? { description: ps.description } : {}), + ...(banner ? { bannerId: banner.id } : {}), + }); + + return await Channels.pack(channel.id, me); +}); diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts index 02d59682b8..bceb9548ef 100644 --- a/src/server/api/endpoints/i.ts +++ b/src/server/api/endpoints/i.ts @@ -24,7 +24,6 @@ export default define(meta, async (ps, user, token) => { return await Users.pack(user, user, { detail: true, - includeHasUnreadNotes: true, includeSecrets: isSecure }); }); diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts index 5076dad82a..b8c4900af7 100644 --- a/src/server/api/endpoints/notes/create.ts +++ b/src/server/api/endpoints/notes/create.ts @@ -7,11 +7,12 @@ import { fetchMeta } from '../../../../misc/fetch-meta'; import { ApiError } from '../../error'; import { ID } from '../../../../misc/cafy-id'; import { User } from '../../../../models/entities/user'; -import { Users, DriveFiles, Notes } from '../../../../models'; +import { Users, DriveFiles, Notes, Channels } from '../../../../models'; import { DriveFile } from '../../../../models/entities/drive-file'; import { Note } from '../../../../models/entities/note'; import { DB_MAX_NOTE_TEXT_LENGTH } from '../../../../misc/hard-limits'; import { noteVisibilities } from '../../../../types'; +import { Channel } from '../../../../models/entities/channel'; let maxNoteTextLength = 500; @@ -128,19 +129,26 @@ export const meta = { }, replyId: { - validator: $.optional.type(ID), + validator: $.optional.nullable.type(ID), desc: { 'ja-JP': '返信対象' } }, renoteId: { - validator: $.optional.type(ID), + validator: $.optional.nullable.type(ID), desc: { 'ja-JP': 'Renote対象' } }, + channelId: { + validator: $.optional.nullable.type(ID), + desc: { + 'ja-JP': 'チャンネル' + } + }, + poll: { validator: $.optional.obj({ choices: $.arr($.str) @@ -206,7 +214,13 @@ export const meta = { message: 'Poll is already expired.', code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL', id: '04da457d-b083-4055-9082-955525eda5a5' - } + }, + + noSuchChannel: { + message: 'No such channel.', + code: 'NO_SUCH_CHANNEL', + id: 'b1653923-5453-4edc-b786-7c4f39bb0bbb' + }, } }; @@ -269,6 +283,15 @@ export default define(meta, async (ps, user) => { throw new ApiError(meta.errors.contentRequired); } + let channel: Channel | undefined; + if (ps.channelId != null) { + channel = await Channels.findOne(ps.channelId); + + if (channel == null) { + throw new ApiError(meta.errors.noSuchChannel); + } + } + // 投稿を作成 const note = await create(user, { createdAt: new Date(), @@ -286,6 +309,7 @@ export default define(meta, async (ps, user) => { localOnly: ps.localOnly, visibility: ps.visibility, visibleUsers, + channel, apMentions: ps.noExtractMentions ? [] : undefined, apHashtags: ps.noExtractHashtags ? [] : undefined, apEmojis: ps.noExtractEmojis ? [] : undefined, diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts index 5e61c17841..6d99f1fdbc 100644 --- a/src/server/api/endpoints/notes/global-timeline.ts +++ b/src/server/api/endpoints/notes/global-timeline.ts @@ -80,6 +80,7 @@ export default define(meta, async (ps, user) => { const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('note.visibility = \'public\'') + .andWhere('note.channelId IS NULL') .leftJoinAndSelect('note.user', 'user'); generateRepliesQuery(query, user); diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts index fab4e9f4e5..2b91b8c67b 100644 --- a/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -13,6 +13,7 @@ import { generateRepliesQuery } from '../../common/generate-replies-query'; import { injectPromo } from '../../common/inject-promo'; import { injectFeatured } from '../../common/inject-featured'; import { generateMutedNoteQuery } from '../../common/generate-muted-note-query'; +import { generateChannelQuery } from '../../common/generate-channel-query'; export const meta = { desc: { @@ -131,6 +132,7 @@ export default define(meta, async (ps, user) => { .leftJoinAndSelect('note.user', 'user') .setParameters(followingQuery.getParameters()); + generateChannelQuery(query, user); generateRepliesQuery(query, user); generateVisibilityQuery(query, user); generateMutedUserQuery(query, user); diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts index 38ec1d4727..51e35e6241 100644 --- a/src/server/api/endpoints/notes/local-timeline.ts +++ b/src/server/api/endpoints/notes/local-timeline.ts @@ -13,6 +13,7 @@ import { generateRepliesQuery } from '../../common/generate-replies-query'; import { injectPromo } from '../../common/inject-promo'; import { injectFeatured } from '../../common/inject-featured'; import { generateMutedNoteQuery } from '../../common/generate-muted-note-query'; +import { generateChannelQuery } from '../../common/generate-channel-query'; export const meta = { desc: { @@ -99,6 +100,7 @@ export default define(meta, async (ps, user) => { .andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)') .leftJoinAndSelect('note.user', 'user'); + generateChannelQuery(query, user); generateRepliesQuery(query, user); generateVisibilityQuery(query, user); if (user) generateMutedUserQuery(query, user); diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts index 657739820b..f09f3d1733 100644 --- a/src/server/api/endpoints/notes/timeline.ts +++ b/src/server/api/endpoints/notes/timeline.ts @@ -11,6 +11,7 @@ import { generateRepliesQuery } from '../../common/generate-replies-query'; import { injectPromo } from '../../common/inject-promo'; import { injectFeatured } from '../../common/inject-featured'; import { generateMutedNoteQuery } from '../../common/generate-muted-note-query'; +import { generateChannelQuery } from '../../common/generate-channel-query'; export const meta = { desc: { @@ -124,6 +125,7 @@ export default define(meta, async (ps, user) => { .leftJoinAndSelect('note.user', 'user') .setParameters(followingQuery.getParameters()); + generateChannelQuery(query, user); generateRepliesQuery(query, user); generateVisibilityQuery(query, user); generateMutedUserQuery(query, user); diff --git a/src/server/api/stream/channel.ts b/src/server/api/stream/channel.ts index 82a95ad3d7..9b7c31e7bb 100644 --- a/src/server/api/stream/channel.ts +++ b/src/server/api/stream/channel.ts @@ -27,6 +27,10 @@ export default abstract class Channel { return this.connection.muting; } + protected get followingChannels() { + return this.connection.followingChannels; + } + protected get subscriber() { return this.connection.subscriber; } diff --git a/src/server/api/stream/channels/channel.ts b/src/server/api/stream/channels/channel.ts new file mode 100644 index 0000000000..c24b3db937 --- /dev/null +++ b/src/server/api/stream/channels/channel.ts @@ -0,0 +1,49 @@ +import autobind from 'autobind-decorator'; +import Channel from '../channel'; +import { Notes } from '../../../../models'; +import { isMutedUserRelated } from '../../../../misc/is-muted-user-related'; +import { PackedNote } from '../../../../models/repositories/note'; + +export default class extends Channel { + public readonly chName = 'channel'; + public static shouldShare = false; + public static requireCredential = false; + private channelId: string; + + @autobind + public async init(params: any) { + this.channelId = params.channelId as string; + + // Subscribe stream + this.subscriber.on('notesStream', this.onNote); + } + + @autobind + private async onNote(note: PackedNote) { + if (note.channelId !== this.channelId) return; + + // リプライなら再pack + if (note.replyId != null) { + note.reply = await Notes.pack(note.replyId, this.user, { + detail: true + }); + } + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await Notes.pack(note.renoteId, this.user, { + detail: true + }); + } + + // 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する + if (isMutedUserRelated(note, this.muting)) return; + + this.send('note', note); + } + + @autobind + public dispose() { + // Unsubscribe events + this.subscriber.off('notesStream', this.onNote); + } +} diff --git a/src/server/api/stream/channels/global-timeline.ts b/src/server/api/stream/channels/global-timeline.ts index d530907d8d..8c97e67226 100644 --- a/src/server/api/stream/channels/global-timeline.ts +++ b/src/server/api/stream/channels/global-timeline.ts @@ -25,6 +25,7 @@ export default class extends Channel { @autobind private async onNote(note: PackedNote) { if (note.visibility !== 'public') return; + if (note.channelId != null) return; // リプライなら再pack if (note.replyId != null) { diff --git a/src/server/api/stream/channels/home-timeline.ts b/src/server/api/stream/channels/home-timeline.ts index caf4ccf5e9..15fe7fa6f0 100644 --- a/src/server/api/stream/channels/home-timeline.ts +++ b/src/server/api/stream/channels/home-timeline.ts @@ -18,8 +18,12 @@ export default class extends Channel { @autobind private async onNote(note: PackedNote) { - // その投稿のユーザーをフォローしていなかったら弾く - if (this.user!.id !== note.userId && !this.following.includes(note.userId)) return; + if (note.channelId) { + if (!this.followingChannels.includes(note.channelId)) return; + } else { + // その投稿のユーザーをフォローしていなかったら弾く + if ((this.user!.id !== note.userId) && !this.following.includes(note.userId)) return; + } if (['followers', 'specified'].includes(note.visibility)) { note = await Notes.pack(note.id, this.user!, { diff --git a/src/server/api/stream/channels/hybrid-timeline.ts b/src/server/api/stream/channels/hybrid-timeline.ts index 1aec98aa72..4dc5f01a32 100644 --- a/src/server/api/stream/channels/hybrid-timeline.ts +++ b/src/server/api/stream/channels/hybrid-timeline.ts @@ -23,11 +23,15 @@ export default class extends Channel { @autobind private async onNote(note: PackedNote) { - // 自分自身の投稿 または その投稿のユーザーをフォローしている または 全体公開のローカルの投稿 の場合だけ + // チャンネルの投稿ではなく、自分自身の投稿 または + // チャンネルの投稿ではなく、その投稿のユーザーをフォローしている または + // チャンネルの投稿ではなく、全体公開のローカルの投稿 または + // フォローしているチャンネルの投稿 の場合だけ if (!( - this.user!.id === note.userId || - this.following.includes(note.userId) || - ((note.user as PackedUser).host == null && note.visibility === 'public') + (note.channelId == null && this.user!.id === note.userId) || + (note.channelId == null && this.following.includes(note.userId)) || + (note.channelId == null && ((note.user as PackedUser).host == null && note.visibility === 'public')) || + (note.channelId != null && this.followingChannels.includes(note.channelId)) )) return; if (['followers', 'specified'].includes(note.visibility)) { diff --git a/src/server/api/stream/channels/index.ts b/src/server/api/stream/channels/index.ts index 6efad078c6..1841573043 100644 --- a/src/server/api/stream/channels/index.ts +++ b/src/server/api/stream/channels/index.ts @@ -11,6 +11,7 @@ import messaging from './messaging'; import messagingIndex from './messaging-index'; import drive from './drive'; import hashtag from './hashtag'; +import channel from './channel'; import admin from './admin'; import gamesReversi from './games/reversi'; import gamesReversiGame from './games/reversi-game'; @@ -29,6 +30,7 @@ export default { messagingIndex, drive, hashtag, + channel, admin, gamesReversi, gamesReversiGame diff --git a/src/server/api/stream/channels/local-timeline.ts b/src/server/api/stream/channels/local-timeline.ts index 6426ccc23f..baeae86603 100644 --- a/src/server/api/stream/channels/local-timeline.ts +++ b/src/server/api/stream/channels/local-timeline.ts @@ -27,6 +27,7 @@ export default class extends Channel { private async onNote(note: PackedNote) { if ((note.user as PackedUser).host !== null) return; if (note.visibility !== 'public') return; + if (note.channelId != null && !this.followingChannels.includes(note.channelId)) return; // リプライなら再pack if (note.replyId != null) { diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts index bebf88a7cd..d420c6e794 100644 --- a/src/server/api/stream/index.ts +++ b/src/server/api/stream/index.ts @@ -7,7 +7,8 @@ import Channel from './channel'; import channels from './channels'; import { EventEmitter } from 'events'; import { User } from '../../../models/entities/user'; -import { Users, Followings, Mutings, UserProfiles } from '../../../models'; +import { Channel as ChannelModel } from '../../../models/entities/channel'; +import { Users, Followings, Mutings, UserProfiles, ChannelFollowings } from '../../../models'; import { ApiError } from '../error'; import { AccessToken } from '../../../models/entities/access-token'; import { UserProfile } from '../../../models/entities/user-profile'; @@ -20,6 +21,7 @@ export default class Connection { public userProfile?: UserProfile; public following: User['id'][] = []; public muting: User['id'][] = []; + public followingChannels: ChannelModel['id'][] = []; public token?: AccessToken; private wsConnection: websocket.connection; public subscriber: EventEmitter; @@ -27,6 +29,7 @@ export default class Connection { private subscribingNotes: any = {}; private followingClock: NodeJS.Timer; private mutingClock: NodeJS.Timer; + private followingChannelsClock: NodeJS.Timer; private userProfileClock: NodeJS.Timer; constructor( @@ -53,6 +56,9 @@ export default class Connection { this.updateMuting(); this.mutingClock = setInterval(this.updateMuting, 5000); + this.updateFollowingChannels(); + this.followingChannelsClock = setInterval(this.updateFollowingChannels, 5000); + this.updateUserProfile(); this.userProfileClock = setInterval(this.updateUserProfile, 5000); } @@ -269,6 +275,18 @@ export default class Connection { } @autobind + private async updateFollowingChannels() { + const followings = await ChannelFollowings.find({ + where: { + followerId: this.user!.id + }, + select: ['followeeId'] + }); + + this.followingChannels = followings.map(x => x.followeeId); + } + + @autobind private async updateUserProfile() { this.userProfile = await UserProfiles.findOne({ userId: this.user!.id @@ -286,6 +304,7 @@ export default class Connection { if (this.followingClock) clearInterval(this.followingClock); if (this.mutingClock) clearInterval(this.mutingClock); + if (this.followingChannelsClock) clearInterval(this.followingChannelsClock); if (this.userProfileClock) clearInterval(this.userProfileClock); } } |