summaryrefslogtreecommitdiff
path: root/src/server/api
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2020-08-18 22:44:21 +0900
committerGitHub <noreply@github.com>2020-08-18 22:44:21 +0900
commit9855405b8989713b81709fc1700e2ead97423467 (patch)
tree54254d2159378d1903e962f0fb37c799bb0f4464 /src/server/api
parentSign (request-target) Fix #6652 (#6656) (diff)
downloadsharkey-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')
-rw-r--r--src/server/api/common/generate-channel-query.ts24
-rw-r--r--src/server/api/endpoints/channels/create.ts68
-rw-r--r--src/server/api/endpoints/channels/featured.ts28
-rw-r--r--src/server/api/endpoints/channels/follow.ts45
-rw-r--r--src/server/api/endpoints/channels/followed.ts28
-rw-r--r--src/server/api/endpoints/channels/owned.ts28
-rw-r--r--src/server/api/endpoints/channels/pin-note.ts0
-rw-r--r--src/server/api/endpoints/channels/show.ts43
-rw-r--r--src/server/api/endpoints/channels/timeline.ts99
-rw-r--r--src/server/api/endpoints/channels/unfollow.ts42
-rw-r--r--src/server/api/endpoints/channels/update.ts93
-rw-r--r--src/server/api/endpoints/i.ts1
-rw-r--r--src/server/api/endpoints/notes/create.ts32
-rw-r--r--src/server/api/endpoints/notes/global-timeline.ts1
-rw-r--r--src/server/api/endpoints/notes/hybrid-timeline.ts2
-rw-r--r--src/server/api/endpoints/notes/local-timeline.ts2
-rw-r--r--src/server/api/endpoints/notes/timeline.ts2
-rw-r--r--src/server/api/stream/channel.ts4
-rw-r--r--src/server/api/stream/channels/channel.ts49
-rw-r--r--src/server/api/stream/channels/global-timeline.ts1
-rw-r--r--src/server/api/stream/channels/home-timeline.ts8
-rw-r--r--src/server/api/stream/channels/hybrid-timeline.ts12
-rw-r--r--src/server/api/stream/channels/index.ts2
-rw-r--r--src/server/api/stream/channels/local-timeline.ts1
-rw-r--r--src/server/api/stream/index.ts21
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);
}
}