From a1b490afa756a71b9cef4afa424575bc223bc612 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 8 Apr 2018 02:30:37 +0900 Subject: Post --> Note Closes #1411 --- src/server/api/endpoints/notes/create.ts | 251 +++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 src/server/api/endpoints/notes/create.ts (limited to 'src/server/api/endpoints/notes/create.ts') diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts new file mode 100644 index 0000000000..7e79912b1b --- /dev/null +++ b/src/server/api/endpoints/notes/create.ts @@ -0,0 +1,251 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import deepEqual = require('deep-equal'); +import Note, { INote, isValidText, isValidCw, pack } from '../../../../models/note'; +import { ILocalUser } from '../../../../models/user'; +import Channel, { IChannel } from '../../../../models/channel'; +import DriveFile from '../../../../models/drive-file'; +import create from '../../../../services/note/create'; +import { IApp } from '../../../../models/app'; + +/** + * Create a note + * + * @param {any} params + * @param {any} user + * @param {any} app + * @return {Promise} + */ +module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res, rej) => { + // Get 'visibility' parameter + const [visibility = 'public', visibilityErr] = $(params.visibility).optional.string().or(['public', 'unlisted', 'private', 'direct']).$; + if (visibilityErr) return rej('invalid visibility'); + + // Get 'text' parameter + const [text, textErr] = $(params.text).optional.string().pipe(isValidText).$; + if (textErr) return rej('invalid text'); + + // Get 'cw' parameter + const [cw, cwErr] = $(params.cw).optional.string().pipe(isValidCw).$; + if (cwErr) return rej('invalid cw'); + + // Get 'viaMobile' parameter + const [viaMobile = false, viaMobileErr] = $(params.viaMobile).optional.boolean().$; + if (viaMobileErr) return rej('invalid viaMobile'); + + // Get 'tags' parameter + const [tags = [], tagsErr] = $(params.tags).optional.array('string').unique().eachQ(t => t.range(1, 32)).$; + if (tagsErr) return rej('invalid tags'); + + // Get 'geo' parameter + const [geo, geoErr] = $(params.geo).optional.nullable.strict.object() + .have('coordinates', $().array().length(2) + .item(0, $().number().range(-180, 180)) + .item(1, $().number().range(-90, 90))) + .have('altitude', $().nullable.number()) + .have('accuracy', $().nullable.number()) + .have('altitudeAccuracy', $().nullable.number()) + .have('heading', $().nullable.number().range(0, 360)) + .have('speed', $().nullable.number()) + .$; + if (geoErr) return rej('invalid geo'); + + // Get 'mediaIds' parameter + const [mediaIds, mediaIdsErr] = $(params.mediaIds).optional.array('id').unique().range(1, 4).$; + if (mediaIdsErr) return rej('invalid mediaIds'); + + let files = []; + if (mediaIds !== undefined) { + // Fetch files + // forEach だと途中でエラーなどがあっても return できないので + // 敢えて for を使っています。 + for (const mediaId of mediaIds) { + // Fetch file + // SELECT _id + const entity = await DriveFile.findOne({ + _id: mediaId, + 'metadata.userId': user._id + }); + + if (entity === null) { + return rej('file not found'); + } else { + files.push(entity); + } + } + } else { + files = null; + } + + // Get 'renoteId' parameter + const [renoteId, renoteIdErr] = $(params.renoteId).optional.id().$; + if (renoteIdErr) return rej('invalid renoteId'); + + let renote: INote = null; + let isQuote = false; + if (renoteId !== undefined) { + // Fetch renote to note + renote = await Note.findOne({ + _id: renoteId + }); + + if (renote == null) { + return rej('renoteee is not found'); + } else if (renote.renoteId && !renote.text && !renote.mediaIds) { + return rej('cannot renote to renote'); + } + + // Fetch recently note + const latestNote = await Note.findOne({ + userId: user._id + }, { + sort: { + _id: -1 + } + }); + + isQuote = text != null || files != null; + + // 直近と同じRenote対象かつ引用じゃなかったらエラー + if (latestNote && + latestNote.renoteId && + latestNote.renoteId.equals(renote._id) && + !isQuote) { + return rej('cannot renote same note that already reposted in your latest note'); + } + + // 直近がRenote対象かつ引用じゃなかったらエラー + if (latestNote && + latestNote._id.equals(renote._id) && + !isQuote) { + return rej('cannot renote your latest note'); + } + } + + // Get 'replyId' parameter + const [replyId, replyIdErr] = $(params.replyId).optional.id().$; + if (replyIdErr) return rej('invalid replyId'); + + let reply: INote = null; + if (replyId !== undefined) { + // Fetch reply + reply = await Note.findOne({ + _id: replyId + }); + + if (reply === null) { + return rej('in reply to note is not found'); + } + + // 返信対象が引用でないRenoteだったらエラー + if (reply.renoteId && !reply.text && !reply.mediaIds) { + return rej('cannot reply to renote'); + } + } + + // Get 'channelId' parameter + const [channelId, channelIdErr] = $(params.channelId).optional.id().$; + if (channelIdErr) return rej('invalid channelId'); + + let channel: IChannel = null; + if (channelId !== undefined) { + // Fetch channel + channel = await Channel.findOne({ + _id: channelId + }); + + if (channel === null) { + return rej('channel not found'); + } + + // 返信対象の投稿がこのチャンネルじゃなかったらダメ + if (reply && !channelId.equals(reply.channelId)) { + return rej('チャンネル内部からチャンネル外部の投稿に返信することはできません'); + } + + // Renote対象の投稿がこのチャンネルじゃなかったらダメ + if (renote && !channelId.equals(renote.channelId)) { + return rej('チャンネル内部からチャンネル外部の投稿をRenoteすることはできません'); + } + + // 引用ではないRenoteはダメ + if (renote && !isQuote) { + return rej('チャンネル内部では引用ではないRenoteをすることはできません'); + } + } else { + // 返信対象の投稿がチャンネルへの投稿だったらダメ + if (reply && reply.channelId != null) { + return rej('チャンネル外部からチャンネル内部の投稿に返信することはできません'); + } + + // Renote対象の投稿がチャンネルへの投稿だったらダメ + if (renote && renote.channelId != null) { + return rej('チャンネル外部からチャンネル内部の投稿をRenoteすることはできません'); + } + } + + // Get 'poll' parameter + const [poll, pollErr] = $(params.poll).optional.strict.object() + .have('choices', $().array('string') + .unique() + .range(2, 10) + .each(c => c.length > 0 && c.length < 50)) + .$; + if (pollErr) return rej('invalid poll'); + + if (poll) { + (poll as any).choices = (poll as any).choices.map((choice, i) => ({ + id: i, // IDを付与 + text: choice.trim(), + votes: 0 + })); + } + + // テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー + if (text === undefined && files === null && renote === null && poll === undefined) { + return rej('text, mediaIds, renoteId or poll is required'); + } + + // 直近の投稿と重複してたらエラー + // TODO: 直近の投稿が一日前くらいなら重複とは見なさない + if (user.latestNote) { + if (deepEqual({ + text: user.latestNote.text, + reply: user.latestNote.replyId ? user.latestNote.replyId.toString() : null, + renote: user.latestNote.renoteId ? user.latestNote.renoteId.toString() : null, + mediaIds: (user.latestNote.mediaIds || []).map(id => id.toString()) + }, { + text: text, + reply: reply ? reply._id.toString() : null, + renote: renote ? renote._id.toString() : null, + mediaIds: (files || []).map(file => file._id.toString()) + })) { + return rej('duplicate'); + } + } + + // 投稿を作成 + const note = await create(user, { + createdAt: new Date(), + media: files, + poll: poll, + text: text, + reply, + renote, + cw: cw, + tags: tags, + app: app, + viaMobile: viaMobile, + visibility, + geo + }); + + const noteObj = await pack(note, user); + + // Reponse + res({ + createdNote: noteObj + }); +}); -- cgit v1.2.3-freya From 50d56bdc2585a3bbd4ad996426f62d6ed186a34d Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 17 Apr 2018 22:31:52 +0900 Subject: Clean up --- src/models/user.ts | 4 --- src/server/api/endpoints/notes/create.ts | 42 -------------------------------- src/services/note/create.ts | 6 +---- 3 files changed, 1 insertion(+), 51 deletions(-) (limited to 'src/server/api/endpoints/notes/create.ts') diff --git a/src/models/user.ts b/src/models/user.ts index 9638d15932..00c249849e 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -47,7 +47,6 @@ type IUserBase = { bannerId: mongo.ObjectID; data: any; description: string; - latestNote: INote; pinnedNoteId: mongo.ObjectID; isSuspended: boolean; keywords: string[]; @@ -332,9 +331,6 @@ export const pack = ( _user.id = _user._id; delete _user._id; - // Remove needless properties - delete _user.latestNote; - if (_user.host == null) { // Remove private properties delete _user.keypair; diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts index 7e79912b1b..4435a6a3c8 100644 --- a/src/server/api/endpoints/notes/create.ts +++ b/src/server/api/endpoints/notes/create.ts @@ -97,31 +97,7 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res return rej('cannot renote to renote'); } - // Fetch recently note - const latestNote = await Note.findOne({ - userId: user._id - }, { - sort: { - _id: -1 - } - }); - isQuote = text != null || files != null; - - // 直近と同じRenote対象かつ引用じゃなかったらエラー - if (latestNote && - latestNote.renoteId && - latestNote.renoteId.equals(renote._id) && - !isQuote) { - return rej('cannot renote same note that already reposted in your latest note'); - } - - // 直近がRenote対象かつ引用じゃなかったらエラー - if (latestNote && - latestNote._id.equals(renote._id) && - !isQuote) { - return rej('cannot renote your latest note'); - } } // Get 'replyId' parameter @@ -208,24 +184,6 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res return rej('text, mediaIds, renoteId or poll is required'); } - // 直近の投稿と重複してたらエラー - // TODO: 直近の投稿が一日前くらいなら重複とは見なさない - if (user.latestNote) { - if (deepEqual({ - text: user.latestNote.text, - reply: user.latestNote.replyId ? user.latestNote.replyId.toString() : null, - renote: user.latestNote.renoteId ? user.latestNote.renoteId.toString() : null, - mediaIds: (user.latestNote.mediaIds || []).map(id => id.toString()) - }, { - text: text, - reply: reply ? reply._id.toString() : null, - renote: renote ? renote._id.toString() : null, - mediaIds: (files || []).map(file => file._id.toString()) - })) { - return rej('duplicate'); - } - } - // 投稿を作成 const note = await create(user, { createdAt: new Date(), diff --git a/src/services/note/create.ts b/src/services/note/create.ts index e35e5ecfbd..391a3d7647 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -89,14 +89,10 @@ export default async (user: IUser, data: { res(note); + // Increment notes count User.update({ _id: user._id }, { - // Increment notes count $inc: { notesCount: 1 - }, - // Update latest note - $set: { - latestNote: note } }); -- cgit v1.2.3-freya From a447ed1d106451b1e55f106d6aa4545680de3b82 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 17 Apr 2018 22:33:27 +0900 Subject: oops --- src/models/user.ts | 2 +- src/server/api/endpoints/notes/create.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) (limited to 'src/server/api/endpoints/notes/create.ts') diff --git a/src/models/user.ts b/src/models/user.ts index 00c249849e..bcb2a73e24 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -2,7 +2,7 @@ import * as mongo from 'mongodb'; import deepcopy = require('deepcopy'); import rap from '@prezzemolo/rap'; import db from '../db/mongodb'; -import Note, { INote, pack as packNote, deleteNote } from './note'; +import Note, { pack as packNote, deleteNote } from './note'; import Following, { deleteFollowing } from './following'; import Mute, { deleteMute } from './mute'; import getFriends from '../server/api/common/get-friends'; diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts index 4435a6a3c8..14e906eb9d 100644 --- a/src/server/api/endpoints/notes/create.ts +++ b/src/server/api/endpoints/notes/create.ts @@ -2,7 +2,6 @@ * Module dependencies */ import $ from 'cafy'; -import deepEqual = require('deep-equal'); import Note, { INote, isValidText, isValidCw, pack } from '../../../../models/note'; import { ILocalUser } from '../../../../models/user'; import Channel, { IChannel } from '../../../../models/channel'; -- cgit v1.2.3-freya From d2d3a7d52b78b7d7d1da4380b062ac2279948cae Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 22 Apr 2018 17:04:52 +0900 Subject: CW --- .../app/desktop/views/components/notes.note.vue | 216 ++++++++++-------- .../app/desktop/views/components/post-form.vue | 77 +++++-- src/client/app/mobile/views/components/note.vue | 250 ++++++++++++--------- .../app/mobile/views/components/post-form.vue | 33 ++- src/models/note.ts | 2 +- src/server/api/endpoints/notes/create.ts | 16 +- src/services/note/create.ts | 2 +- 7 files changed, 369 insertions(+), 227 deletions(-) (limited to 'src/server/api/endpoints/notes/create.ts') diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue index ae2793db92..6d0e0c8f5c 100644 --- a/src/client/app/desktop/views/components/notes.note.vue +++ b/src/client/app/desktop/views/components/notes.note.vue @@ -34,24 +34,30 @@

{{ p.channel.title }}:

-
- %fa:reply% - - RP: -
-
- -
- -
- {{ tag }} -
- %fa:map-marker-alt% 位置情報 -
-
- +

+ {{ p.cw }} + {{ showContent ? '隠す' : 'もっと見る' }} +

+
+
+ %fa:reply% + + RP: +
+
+ +
+ +
+ {{ tag }} +
+ %fa:map-marker-alt% 位置情報 +
+
+ +
+
-