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/endpoints | |
| parent | Sign (request-target) Fix #6652 (#6656) (diff) | |
| download | misskey-9855405b8989713b81709fc1700e2ead97423467.tar.gz misskey-9855405b8989713b81709fc1700e2ead97423467.tar.bz2 misskey-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/endpoints')
| -rw-r--r-- | src/server/api/endpoints/channels/create.ts | 68 | ||||
| -rw-r--r-- | src/server/api/endpoints/channels/featured.ts | 28 | ||||
| -rw-r--r-- | src/server/api/endpoints/channels/follow.ts | 45 | ||||
| -rw-r--r-- | src/server/api/endpoints/channels/followed.ts | 28 | ||||
| -rw-r--r-- | src/server/api/endpoints/channels/owned.ts | 28 | ||||
| -rw-r--r-- | src/server/api/endpoints/channels/pin-note.ts | 0 | ||||
| -rw-r--r-- | src/server/api/endpoints/channels/show.ts | 43 | ||||
| -rw-r--r-- | src/server/api/endpoints/channels/timeline.ts | 99 | ||||
| -rw-r--r-- | src/server/api/endpoints/channels/unfollow.ts | 42 | ||||
| -rw-r--r-- | src/server/api/endpoints/channels/update.ts | 93 | ||||
| -rw-r--r-- | src/server/api/endpoints/i.ts | 1 | ||||
| -rw-r--r-- | src/server/api/endpoints/notes/create.ts | 32 | ||||
| -rw-r--r-- | src/server/api/endpoints/notes/global-timeline.ts | 1 | ||||
| -rw-r--r-- | src/server/api/endpoints/notes/hybrid-timeline.ts | 2 | ||||
| -rw-r--r-- | src/server/api/endpoints/notes/local-timeline.ts | 2 | ||||
| -rw-r--r-- | src/server/api/endpoints/notes/timeline.ts | 2 |
16 files changed, 509 insertions, 5 deletions
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); |