diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2021-11-12 02:02:25 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2021-11-12 02:02:25 +0900 |
| commit | 0e4a111f81cceed275d9bec2695f6e401fb654d8 (patch) | |
| tree | 40874799472fa07416f17b50a398ac33b7771905 /packages/backend/src/server/api/endpoints/notes/create.ts | |
| parent | update deps (diff) | |
| download | sharkey-0e4a111f81cceed275d9bec2695f6e401fb654d8.tar.gz sharkey-0e4a111f81cceed275d9bec2695f6e401fb654d8.tar.bz2 sharkey-0e4a111f81cceed275d9bec2695f6e401fb654d8.zip | |
refactoring
Resolve #7779
Diffstat (limited to 'packages/backend/src/server/api/endpoints/notes/create.ts')
| -rw-r--r-- | packages/backend/src/server/api/endpoints/notes/create.ts | 299 |
1 files changed, 299 insertions, 0 deletions
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts new file mode 100644 index 0000000000..751673f955 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/create.ts @@ -0,0 +1,299 @@ +import $ from 'cafy'; +import * as ms from 'ms'; +import { length } from 'stringz'; +import create from '@/services/note/create'; +import define from '../../define'; +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, Channels, Blockings } from '@/models/index'; +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; + +setInterval(() => { + fetchMeta().then(m => { + maxNoteTextLength = m.maxNoteTextLength; + }); +}, 3000); + +export const meta = { + tags: ['notes'], + + requireCredential: true as const, + + limit: { + duration: ms('1hour'), + max: 300 + }, + + kind: 'write:notes', + + params: { + visibility: { + validator: $.optional.str.or(noteVisibilities as unknown as string[]), + default: 'public', + }, + + visibleUserIds: { + validator: $.optional.arr($.type(ID)).unique().min(0), + }, + + text: { + validator: $.optional.nullable.str.pipe(text => + text.trim() != '' + && length(text.trim()) <= maxNoteTextLength + && Array.from(text.trim()).length <= DB_MAX_NOTE_TEXT_LENGTH // DB limit + ), + default: null, + }, + + cw: { + validator: $.optional.nullable.str.pipe(Notes.validateCw), + }, + + viaMobile: { + validator: $.optional.bool, + default: false, + }, + + localOnly: { + validator: $.optional.bool, + default: false, + }, + + noExtractMentions: { + validator: $.optional.bool, + default: false, + }, + + noExtractHashtags: { + validator: $.optional.bool, + default: false, + }, + + noExtractEmojis: { + validator: $.optional.bool, + default: false, + }, + + fileIds: { + validator: $.optional.arr($.type(ID)).unique().range(1, 4), + }, + + mediaIds: { + validator: $.optional.arr($.type(ID)).unique().range(1, 4), + deprecated: true, + }, + + replyId: { + validator: $.optional.nullable.type(ID), + }, + + renoteId: { + validator: $.optional.nullable.type(ID), + }, + + channelId: { + validator: $.optional.nullable.type(ID), + }, + + poll: { + validator: $.optional.nullable.obj({ + choices: $.arr($.str) + .unique() + .range(2, 10) + .each(c => c.length > 0 && c.length < 50), + multiple: $.optional.bool, + expiresAt: $.optional.nullable.num.int(), + expiredAfter: $.optional.nullable.num.int().min(1) + }).strict(), + ref: 'poll' + } + }, + + res: { + type: 'object' as const, + optional: false as const, nullable: false as const, + properties: { + createdNote: { + type: 'object' as const, + optional: false as const, nullable: false as const, + ref: 'Note', + } + } + }, + + errors: { + noSuchRenoteTarget: { + message: 'No such renote target.', + code: 'NO_SUCH_RENOTE_TARGET', + id: 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4' + }, + + cannotReRenote: { + message: 'You can not Renote a pure Renote.', + code: 'CANNOT_RENOTE_TO_A_PURE_RENOTE', + id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a' + }, + + noSuchReplyTarget: { + message: 'No such reply target.', + code: 'NO_SUCH_REPLY_TARGET', + id: '749ee0f6-d3da-459a-bf02-282e2da4292c' + }, + + cannotReplyToPureRenote: { + message: 'You can not reply to a pure Renote.', + code: 'CANNOT_REPLY_TO_A_PURE_RENOTE', + id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15' + }, + + contentRequired: { + message: 'Content required. You need to set text, fileIds, renoteId or poll.', + code: 'CONTENT_REQUIRED', + id: '6f57e42b-c348-439b-bc45-993995cc515a' + }, + + cannotCreateAlreadyExpiredPoll: { + 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' + }, + + youHaveBeenBlocked: { + message: 'You have been blocked by this user.', + code: 'YOU_HAVE_BEEN_BLOCKED', + id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3' + }, + } +}; + +export default define(meta, async (ps, user) => { + let visibleUsers: User[] = []; + if (ps.visibleUserIds) { + visibleUsers = (await Promise.all(ps.visibleUserIds.map(id => Users.findOne(id)))) + .filter(x => x != null) as User[]; + } + + let files: DriveFile[] = []; + const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null; + if (fileIds != null) { + files = (await Promise.all(fileIds.map(fileId => + DriveFiles.findOne({ + id: fileId, + userId: user.id + }) + ))).filter(file => file != null) as DriveFile[]; + } + + let renote: Note | undefined; + if (ps.renoteId != null) { + // Fetch renote to note + renote = await Notes.findOne(ps.renoteId); + + if (renote == null) { + throw new ApiError(meta.errors.noSuchRenoteTarget); + } else if (renote.renoteId && !renote.text && !renote.fileIds) { + throw new ApiError(meta.errors.cannotReRenote); + } + + // Check blocking + if (renote.userId !== user.id) { + const block = await Blockings.findOne({ + blockerId: renote.userId, + blockeeId: user.id, + }); + if (block) { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } + } + } + + let reply: Note | undefined; + if (ps.replyId != null) { + // Fetch reply + reply = await Notes.findOne(ps.replyId); + + if (reply == null) { + throw new ApiError(meta.errors.noSuchReplyTarget); + } + + // 返信対象が引用でないRenoteだったらエラー + if (reply.renoteId && !reply.text && !reply.fileIds) { + throw new ApiError(meta.errors.cannotReplyToPureRenote); + } + + // Check blocking + if (reply.userId !== user.id) { + const block = await Blockings.findOne({ + blockerId: reply.userId, + blockeeId: user.id, + }); + if (block) { + throw new ApiError(meta.errors.youHaveBeenBlocked); + } + } + } + + if (ps.poll) { + if (typeof ps.poll.expiresAt === 'number') { + if (ps.poll.expiresAt < Date.now()) + throw new ApiError(meta.errors.cannotCreateAlreadyExpiredPoll); + } else if (typeof ps.poll.expiredAfter === 'number') { + ps.poll.expiresAt = Date.now() + ps.poll.expiredAfter; + } + } + + // テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー + if (!(ps.text || files.length || renote || ps.poll)) { + 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(), + files: files, + poll: ps.poll ? { + choices: ps.poll.choices, + multiple: ps.poll.multiple || false, + expiresAt: ps.poll.expiresAt ? new Date(ps.poll.expiresAt) : null + } : undefined, + text: ps.text || undefined, + reply, + renote, + cw: ps.cw, + viaMobile: ps.viaMobile, + localOnly: ps.localOnly, + visibility: ps.visibility, + visibleUsers, + channel, + apMentions: ps.noExtractMentions ? [] : undefined, + apHashtags: ps.noExtractHashtags ? [] : undefined, + apEmojis: ps.noExtractEmojis ? [] : undefined, + }); + + return { + createdNote: await Notes.pack(note, user) + }; +}); |