Post --> Note

Closes #1411
This commit is contained in:
syuilo
2018-04-08 02:30:37 +09:00
parent c7106d250c
commit a1b490afa7
167 changed files with 4440 additions and 1762 deletions

View File

@@ -0,0 +1,63 @@
/**
* Module dependencies
*/
import $ from 'cafy';
import Note, { pack } from '../../../../models/note';
/**
* Show a context of a note
*
* @param {any} params
* @param {any} user
* @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
const [noteId, noteIdErr] = $(params.noteId).id().$;
if (noteIdErr) return rej('invalid noteId param');
// Get 'limit' parameter
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
if (limitErr) return rej('invalid limit param');
// Get 'offset' parameter
const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$;
if (offsetErr) return rej('invalid offset param');
// Lookup note
const note = await Note.findOne({
_id: noteId
});
if (note === null) {
return rej('note not found');
}
const context = [];
let i = 0;
async function get(id) {
i++;
const p = await Note.findOne({ _id: id });
if (i > offset) {
context.push(p);
}
if (context.length == limit) {
return;
}
if (p.replyId) {
await get(p.replyId);
}
}
if (note.replyId) {
await get(note.replyId);
}
// Serialize
res(await Promise.all(context.map(async note =>
await pack(note, user))));
});

View File

@@ -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<any>}
*/
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
});
});

View File

@@ -0,0 +1,48 @@
/**
* Module dependencies
*/
import $ from 'cafy';
import Favorite from '../../../../../models/favorite';
import Note from '../../../../../models/note';
/**
* Favorite a note
*
* @param {any} params
* @param {any} user
* @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
const [noteId, noteIdErr] = $(params.noteId).id().$;
if (noteIdErr) return rej('invalid noteId param');
// Get favoritee
const note = await Note.findOne({
_id: noteId
});
if (note === null) {
return rej('note not found');
}
// if already favorited
const exist = await Favorite.findOne({
noteId: note._id,
userId: user._id
});
if (exist !== null) {
return rej('already favorited');
}
// Create favorite
await Favorite.insert({
createdAt: new Date(),
noteId: note._id,
userId: user._id
});
// Send response
res();
});

View File

@@ -0,0 +1,46 @@
/**
* Module dependencies
*/
import $ from 'cafy';
import Favorite from '../../../../../models/favorite';
import Note from '../../../../../models/note';
/**
* Unfavorite a note
*
* @param {any} params
* @param {any} user
* @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
const [noteId, noteIdErr] = $(params.noteId).id().$;
if (noteIdErr) return rej('invalid noteId param');
// Get favoritee
const note = await Note.findOne({
_id: noteId
});
if (note === null) {
return rej('note not found');
}
// if already favorited
const exist = await Favorite.findOne({
noteId: note._id,
userId: user._id
});
if (exist === null) {
return rej('already not favorited');
}
// Delete favorite
await Favorite.remove({
_id: exist._id
});
// Send response
res();
});

View File

@@ -0,0 +1,78 @@
/**
* Module dependencies
*/
import $ from 'cafy';
import Note from '../../../../models/note';
import getFriends from '../../common/get-friends';
import { pack } from '../../../../models/note';
/**
* Get mentions of myself
*
* @param {any} params
* @param {any} user
* @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'following' parameter
const [following = false, followingError] =
$(params.following).optional.boolean().$;
if (followingError) return rej('invalid following param');
// Get 'limit' parameter
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
if (limitErr) return rej('invalid limit param');
// Get 'sinceId' parameter
const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
if (sinceIdErr) return rej('invalid sinceId param');
// Get 'untilId' parameter
const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
if (untilIdErr) return rej('invalid untilId param');
// Check if both of sinceId and untilId is specified
if (sinceId && untilId) {
return rej('cannot set sinceId and untilId');
}
// Construct query
const query = {
mentions: user._id
} as any;
const sort = {
_id: -1
};
if (following) {
const followingIds = await getFriends(user._id);
query.userId = {
$in: followingIds
};
}
if (sinceId) {
sort._id = 1;
query._id = {
$gt: sinceId
};
} else if (untilId) {
query._id = {
$lt: untilId
};
}
// Issue query
const mentions = await Note
.find(query, {
limit: limit,
sort: sort
});
// Serialize
res(await Promise.all(mentions.map(async mention =>
await pack(mention, user)
)));
});

View File

@@ -0,0 +1,59 @@
/**
* Module dependencies
*/
import $ from 'cafy';
import Vote from '../../../../../models/poll-vote';
import Note, { pack } from '../../../../../models/note';
/**
* Get recommended polls
*
* @param {any} params
* @param {any} user
* @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'limit' parameter
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
if (limitErr) return rej('invalid limit param');
// Get 'offset' parameter
const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$;
if (offsetErr) return rej('invalid offset param');
// Get votes
const votes = await Vote.find({
userId: user._id
}, {
fields: {
_id: false,
noteId: true
}
});
const nin = votes && votes.length != 0 ? votes.map(v => v.noteId) : [];
const notes = await Note
.find({
_id: {
$nin: nin
},
userId: {
$ne: user._id
},
poll: {
$exists: true,
$ne: null
}
}, {
limit: limit,
skip: offset,
sort: {
_id: -1
}
});
// Serialize
res(await Promise.all(notes.map(async note =>
await pack(note, user, { detail: true }))));
});

View File

@@ -0,0 +1,115 @@
/**
* Module dependencies
*/
import $ from 'cafy';
import Vote from '../../../../../models/poll-vote';
import Note from '../../../../../models/note';
import Watching from '../../../../../models/note-watching';
import watch from '../../../../../note/watch';
import { publishNoteStream } from '../../../../../publishers/stream';
import notify from '../../../../../publishers/notify';
/**
* Vote poll of a note
*
* @param {any} params
* @param {any} user
* @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
const [noteId, noteIdErr] = $(params.noteId).id().$;
if (noteIdErr) return rej('invalid noteId param');
// Get votee
const note = await Note.findOne({
_id: noteId
});
if (note === null) {
return rej('note not found');
}
if (note.poll == null) {
return rej('poll not found');
}
// Get 'choice' parameter
const [choice, choiceError] =
$(params.choice).number()
.pipe(c => note.poll.choices.some(x => x.id == c))
.$;
if (choiceError) return rej('invalid choice param');
// if already voted
const exist = await Vote.findOne({
noteId: note._id,
userId: user._id
});
if (exist !== null) {
return rej('already voted');
}
// Create vote
await Vote.insert({
createdAt: new Date(),
noteId: note._id,
userId: user._id,
choice: choice
});
// Send response
res();
const inc = {};
inc[`poll.choices.${findWithAttr(note.poll.choices, 'id', choice)}.votes`] = 1;
// Increment votes count
await Note.update({ _id: note._id }, {
$inc: inc
});
publishNoteStream(note._id, 'poll_voted');
// Notify
notify(note.userId, user._id, 'poll_vote', {
noteId: note._id,
choice: choice
});
// Fetch watchers
Watching
.find({
noteId: note._id,
userId: { $ne: user._id },
// 削除されたドキュメントは除く
deletedAt: { $exists: false }
}, {
fields: {
userId: true
}
})
.then(watchers => {
watchers.forEach(watcher => {
notify(watcher.userId, user._id, 'poll_vote', {
noteId: note._id,
choice: choice
});
});
});
// この投稿をWatchする
if (user.account.settings.autoWatch !== false) {
watch(user._id, note);
}
});
function findWithAttr(array, attr, value) {
for (let i = 0; i < array.length; i += 1) {
if (array[i][attr] === value) {
return i;
}
}
return -1;
}

View File

@@ -0,0 +1,57 @@
/**
* Module dependencies
*/
import $ from 'cafy';
import Note from '../../../../models/note';
import Reaction, { pack } from '../../../../models/note-reaction';
/**
* Show reactions of a note
*
* @param {any} params
* @param {any} user
* @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
const [noteId, noteIdErr] = $(params.noteId).id().$;
if (noteIdErr) return rej('invalid noteId param');
// Get 'limit' parameter
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
if (limitErr) return rej('invalid limit param');
// Get 'offset' parameter
const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$;
if (offsetErr) return rej('invalid offset param');
// Get 'sort' parameter
const [sort = 'desc', sortError] = $(params.sort).optional.string().or('desc asc').$;
if (sortError) return rej('invalid sort param');
// Lookup note
const note = await Note.findOne({
_id: noteId
});
if (note === null) {
return rej('note not found');
}
// Issue query
const reactions = await Reaction
.find({
noteId: note._id,
deletedAt: { $exists: false }
}, {
limit: limit,
skip: offset,
sort: {
_id: sort == 'asc' ? 1 : -1
}
});
// Serialize
res(await Promise.all(reactions.map(async reaction =>
await pack(reaction, user))));
});

View File

@@ -0,0 +1,47 @@
/**
* Module dependencies
*/
import $ from 'cafy';
import Reaction from '../../../../../models/note-reaction';
import Note from '../../../../../models/note';
import create from '../../../../../services/note/reaction/create';
/**
* React to a note
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
const [noteId, noteIdErr] = $(params.noteId).id().$;
if (noteIdErr) return rej('invalid noteId param');
// Get 'reaction' parameter
const [reaction, reactionErr] = $(params.reaction).string().or([
'like',
'love',
'laugh',
'hmm',
'surprise',
'congrats',
'angry',
'confused',
'pudding'
]).$;
if (reactionErr) return rej('invalid reaction param');
// Fetch reactee
const note = await Note.findOne({
_id: noteId
});
if (note === null) {
return rej('note not found');
}
try {
await create(user, note, reaction);
} catch (e) {
rej(e);
}
res();
});

View File

@@ -0,0 +1,60 @@
/**
* Module dependencies
*/
import $ from 'cafy';
import Reaction from '../../../../../models/note-reaction';
import Note from '../../../../../models/note';
// import event from '../../../publishers/stream';
/**
* Unreact to a note
*
* @param {any} params
* @param {any} user
* @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
const [noteId, noteIdErr] = $(params.noteId).id().$;
if (noteIdErr) return rej('invalid noteId param');
// Fetch unreactee
const note = await Note.findOne({
_id: noteId
});
if (note === null) {
return rej('note not found');
}
// if already unreacted
const exist = await Reaction.findOne({
noteId: note._id,
userId: user._id,
deletedAt: { $exists: false }
});
if (exist === null) {
return rej('never reacted');
}
// Delete reaction
await Reaction.update({
_id: exist._id
}, {
$set: {
deletedAt: new Date()
}
});
// Send response
res();
const dec = {};
dec[`reactionCounts.${exist.reaction}`] = -1;
// Decrement reactions count
Note.update({ _id: note._id }, {
$inc: dec
});
});

View File

@@ -0,0 +1,53 @@
/**
* Module dependencies
*/
import $ from 'cafy';
import Note, { pack } from '../../../../models/note';
/**
* Show a replies of a note
*
* @param {any} params
* @param {any} user
* @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
const [noteId, noteIdErr] = $(params.noteId).id().$;
if (noteIdErr) return rej('invalid noteId param');
// Get 'limit' parameter
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
if (limitErr) return rej('invalid limit param');
// Get 'offset' parameter
const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$;
if (offsetErr) return rej('invalid offset param');
// Get 'sort' parameter
const [sort = 'desc', sortError] = $(params.sort).optional.string().or('desc asc').$;
if (sortError) return rej('invalid sort param');
// Lookup note
const note = await Note.findOne({
_id: noteId
});
if (note === null) {
return rej('note not found');
}
// Issue query
const replies = await Note
.find({ replyId: note._id }, {
limit: limit,
skip: offset,
sort: {
_id: sort == 'asc' ? 1 : -1
}
});
// Serialize
res(await Promise.all(replies.map(async note =>
await pack(note, user))));
});

View File

@@ -0,0 +1,73 @@
/**
* Module dependencies
*/
import $ from 'cafy';
import Note, { pack } from '../../../../models/note';
/**
* Show a renotes of a note
*
* @param {any} params
* @param {any} user
* @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
const [noteId, noteIdErr] = $(params.noteId).id().$;
if (noteIdErr) return rej('invalid noteId param');
// Get 'limit' parameter
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
if (limitErr) return rej('invalid limit param');
// Get 'sinceId' parameter
const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
if (sinceIdErr) return rej('invalid sinceId param');
// Get 'untilId' parameter
const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
if (untilIdErr) return rej('invalid untilId param');
// Check if both of sinceId and untilId is specified
if (sinceId && untilId) {
return rej('cannot set sinceId and untilId');
}
// Lookup note
const note = await Note.findOne({
_id: noteId
});
if (note === null) {
return rej('note not found');
}
// Construct query
const sort = {
_id: -1
};
const query = {
renoteId: note._id
} as any;
if (sinceId) {
sort._id = 1;
query._id = {
$gt: sinceId
};
} else if (untilId) {
query._id = {
$lt: untilId
};
}
// Issue query
const renotes = await Note
.find(query, {
limit: limit,
sort: sort
});
// Serialize
res(await Promise.all(renotes.map(async note =>
await pack(note, user))));
});

View File

@@ -0,0 +1,364 @@
/**
* Module dependencies
*/
import $ from 'cafy';
const escapeRegexp = require('escape-regexp');
import Note from '../../../../models/note';
import User from '../../../../models/user';
import Mute from '../../../../models/mute';
import getFriends from '../../common/get-friends';
import { pack } from '../../../../models/note';
/**
* Search a note
*
* @param {any} params
* @param {any} me
* @return {Promise<any>}
*/
module.exports = (params, me) => new Promise(async (res, rej) => {
// Get 'text' parameter
const [text, textError] = $(params.text).optional.string().$;
if (textError) return rej('invalid text param');
// Get 'includeUserIds' parameter
const [includeUserIds = [], includeUserIdsErr] = $(params.includeUserIds).optional.array('id').$;
if (includeUserIdsErr) return rej('invalid includeUserIds param');
// Get 'excludeUserIds' parameter
const [excludeUserIds = [], excludeUserIdsErr] = $(params.excludeUserIds).optional.array('id').$;
if (excludeUserIdsErr) return rej('invalid excludeUserIds param');
// Get 'includeUserUsernames' parameter
const [includeUserUsernames = [], includeUserUsernamesErr] = $(params.includeUserUsernames).optional.array('string').$;
if (includeUserUsernamesErr) return rej('invalid includeUserUsernames param');
// Get 'excludeUserUsernames' parameter
const [excludeUserUsernames = [], excludeUserUsernamesErr] = $(params.excludeUserUsernames).optional.array('string').$;
if (excludeUserUsernamesErr) return rej('invalid excludeUserUsernames param');
// Get 'following' parameter
const [following = null, followingErr] = $(params.following).optional.nullable.boolean().$;
if (followingErr) return rej('invalid following param');
// Get 'mute' parameter
const [mute = 'mute_all', muteErr] = $(params.mute).optional.string().$;
if (muteErr) return rej('invalid mute param');
// Get 'reply' parameter
const [reply = null, replyErr] = $(params.reply).optional.nullable.boolean().$;
if (replyErr) return rej('invalid reply param');
// Get 'renote' parameter
const [renote = null, renoteErr] = $(params.renote).optional.nullable.boolean().$;
if (renoteErr) return rej('invalid renote param');
// Get 'media' parameter
const [media = null, mediaErr] = $(params.media).optional.nullable.boolean().$;
if (mediaErr) return rej('invalid media param');
// Get 'poll' parameter
const [poll = null, pollErr] = $(params.poll).optional.nullable.boolean().$;
if (pollErr) return rej('invalid poll param');
// Get 'sinceDate' parameter
const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$;
if (sinceDateErr) throw 'invalid sinceDate param';
// Get 'untilDate' parameter
const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$;
if (untilDateErr) throw 'invalid untilDate param';
// Get 'offset' parameter
const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$;
if (offsetErr) return rej('invalid offset param');
// Get 'limit' parameter
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 30).$;
if (limitErr) return rej('invalid limit param');
let includeUsers = includeUserIds;
if (includeUserUsernames != null) {
const ids = (await Promise.all(includeUserUsernames.map(async (username) => {
const _user = await User.findOne({
usernameLower: username.toLowerCase()
});
return _user ? _user._id : null;
}))).filter(id => id != null);
includeUsers = includeUsers.concat(ids);
}
let excludeUsers = excludeUserIds;
if (excludeUserUsernames != null) {
const ids = (await Promise.all(excludeUserUsernames.map(async (username) => {
const _user = await User.findOne({
usernameLower: username.toLowerCase()
});
return _user ? _user._id : null;
}))).filter(id => id != null);
excludeUsers = excludeUsers.concat(ids);
}
search(res, rej, me, text, includeUsers, excludeUsers, following,
mute, reply, renote, media, poll, sinceDate, untilDate, offset, limit);
});
async function search(
res, rej, me, text, includeUserIds, excludeUserIds, following,
mute, reply, renote, media, poll, sinceDate, untilDate, offset, max) {
let q: any = {
$and: []
};
const push = x => q.$and.push(x);
if (text) {
// 完全一致検索
if (/"""(.+?)"""/.test(text)) {
const x = text.match(/"""(.+?)"""/)[1];
push({
text: x
});
} else {
const tags = text.split(' ').filter(x => x[0] == '#');
if (tags) {
push({
$and: tags.map(x => ({
tags: x
}))
});
}
push({
$and: text.split(' ').map(x => ({
// キーワードが-で始まる場合そのキーワードを除外する
text: x[0] == '-' ? {
$not: new RegExp(escapeRegexp(x.substr(1)))
} : new RegExp(escapeRegexp(x))
}))
});
}
}
if (includeUserIds && includeUserIds.length != 0) {
push({
userId: {
$in: includeUserIds
}
});
} else if (excludeUserIds && excludeUserIds.length != 0) {
push({
userId: {
$nin: excludeUserIds
}
});
}
if (following != null && me != null) {
const ids = await getFriends(me._id, false);
push({
userId: following ? {
$in: ids
} : {
$nin: ids.concat(me._id)
}
});
}
if (me != null) {
const mutes = await Mute.find({
muterId: me._id,
deletedAt: { $exists: false }
});
const mutedUserIds = mutes.map(m => m.muteeId);
switch (mute) {
case 'mute_all':
push({
userId: {
$nin: mutedUserIds
},
'_reply.userId': {
$nin: mutedUserIds
},
'_renote.userId': {
$nin: mutedUserIds
}
});
break;
case 'mute_related':
push({
'_reply.userId': {
$nin: mutedUserIds
},
'_renote.userId': {
$nin: mutedUserIds
}
});
break;
case 'mute_direct':
push({
userId: {
$nin: mutedUserIds
}
});
break;
case 'direct_only':
push({
userId: {
$in: mutedUserIds
}
});
break;
case 'related_only':
push({
$or: [{
'_reply.userId': {
$in: mutedUserIds
}
}, {
'_renote.userId': {
$in: mutedUserIds
}
}]
});
break;
case 'all_only':
push({
$or: [{
userId: {
$in: mutedUserIds
}
}, {
'_reply.userId': {
$in: mutedUserIds
}
}, {
'_renote.userId': {
$in: mutedUserIds
}
}]
});
break;
}
}
if (reply != null) {
if (reply) {
push({
replyId: {
$exists: true,
$ne: null
}
});
} else {
push({
$or: [{
replyId: {
$exists: false
}
}, {
replyId: null
}]
});
}
}
if (renote != null) {
if (renote) {
push({
renoteId: {
$exists: true,
$ne: null
}
});
} else {
push({
$or: [{
renoteId: {
$exists: false
}
}, {
renoteId: null
}]
});
}
}
if (media != null) {
if (media) {
push({
mediaIds: {
$exists: true,
$ne: null
}
});
} else {
push({
$or: [{
mediaIds: {
$exists: false
}
}, {
mediaIds: null
}]
});
}
}
if (poll != null) {
if (poll) {
push({
poll: {
$exists: true,
$ne: null
}
});
} else {
push({
$or: [{
poll: {
$exists: false
}
}, {
poll: null
}]
});
}
}
if (sinceDate) {
push({
createdAt: {
$gt: new Date(sinceDate)
}
});
}
if (untilDate) {
push({
createdAt: {
$lt: new Date(untilDate)
}
});
}
if (q.$and.length == 0) {
q = {};
}
// Search notes
const notes = await Note
.find(q, {
sort: {
_id: -1
},
limit: max,
skip: offset
});
// Serialize
res(await Promise.all(notes.map(async note =>
await pack(note, me))));
}

View File

@@ -0,0 +1,32 @@
/**
* Module dependencies
*/
import $ from 'cafy';
import Note, { pack } from '../../../../models/note';
/**
* Show a note
*
* @param {any} params
* @param {any} user
* @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'noteId' parameter
const [noteId, noteIdErr] = $(params.noteId).id().$;
if (noteIdErr) return rej('invalid noteId param');
// Get note
const note = await Note.findOne({
_id: noteId
});
if (note === null) {
return rej('note not found');
}
// Serialize
res(await pack(note, user, {
detail: true
}));
});

View File

@@ -0,0 +1,132 @@
/**
* Module dependencies
*/
import $ from 'cafy';
import rap from '@prezzemolo/rap';
import Note from '../../../../models/note';
import Mute from '../../../../models/mute';
import ChannelWatching from '../../../../models/channel-watching';
import getFriends from '../../common/get-friends';
import { pack } from '../../../../models/note';
/**
* Get timeline of myself
*
* @param {any} params
* @param {any} user
* @param {any} app
* @return {Promise<any>}
*/
module.exports = async (params, user, app) => {
// Get 'limit' parameter
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
if (limitErr) throw 'invalid limit param';
// Get 'sinceId' parameter
const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
if (sinceIdErr) throw 'invalid sinceId param';
// Get 'untilId' parameter
const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
if (untilIdErr) throw 'invalid untilId param';
// Get 'sinceDate' parameter
const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$;
if (sinceDateErr) throw 'invalid sinceDate param';
// Get 'untilDate' parameter
const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$;
if (untilDateErr) throw 'invalid untilDate param';
// Check if only one of sinceId, untilId, sinceDate, untilDate specified
if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
}
const { followingIds, watchingChannelIds, mutedUserIds } = await rap({
// ID list of the user itself and other users who the user follows
followingIds: getFriends(user._id),
// Watchしているチャンネルを取得
watchingChannelIds: ChannelWatching.find({
userId: user._id,
// 削除されたドキュメントは除く
deletedAt: { $exists: false }
}).then(watches => watches.map(w => w.channelId)),
// ミュートしているユーザーを取得
mutedUserIds: Mute.find({
muterId: user._id,
// 削除されたドキュメントは除く
deletedAt: { $exists: false }
}).then(ms => ms.map(m => m.muteeId))
});
//#region Construct query
const sort = {
_id: -1
};
const query = {
$or: [{
// フォローしている人のタイムラインへの投稿
userId: {
$in: followingIds
},
// 「タイムラインへの」投稿に限定するためにチャンネルが指定されていないもののみに限る
$or: [{
channelId: {
$exists: false
}
}, {
channelId: null
}]
}, {
// Watchしているチャンネルへの投稿
channelId: {
$in: watchingChannelIds
}
}],
// mute
userId: {
$nin: mutedUserIds
},
'_reply.userId': {
$nin: mutedUserIds
},
'_renote.userId': {
$nin: mutedUserIds
},
} as any;
if (sinceId) {
sort._id = 1;
query._id = {
$gt: sinceId
};
} else if (untilId) {
query._id = {
$lt: untilId
};
} else if (sinceDate) {
sort._id = 1;
query.createdAt = {
$gt: new Date(sinceDate)
};
} else if (untilDate) {
query.createdAt = {
$lt: new Date(untilDate)
};
}
//#endregion
// Issue query
const timeline = await Note
.find(query, {
limit: limit,
sort: sort
});
// Serialize
return await Promise.all(timeline.map(note => pack(note, user)));
};

View File

@@ -0,0 +1,79 @@
/**
* Module dependencies
*/
const ms = require('ms');
import $ from 'cafy';
import Note, { pack } from '../../../../models/note';
/**
* Get trend notes
*
* @param {any} params
* @param {any} user
* @return {Promise<any>}
*/
module.exports = (params, user) => new Promise(async (res, rej) => {
// Get 'limit' parameter
const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
if (limitErr) return rej('invalid limit param');
// Get 'offset' parameter
const [offset = 0, offsetErr] = $(params.offset).optional.number().min(0).$;
if (offsetErr) return rej('invalid offset param');
// Get 'reply' parameter
const [reply, replyErr] = $(params.reply).optional.boolean().$;
if (replyErr) return rej('invalid reply param');
// Get 'renote' parameter
const [renote, renoteErr] = $(params.renote).optional.boolean().$;
if (renoteErr) return rej('invalid renote param');
// Get 'media' parameter
const [media, mediaErr] = $(params.media).optional.boolean().$;
if (mediaErr) return rej('invalid media param');
// Get 'poll' parameter
const [poll, pollErr] = $(params.poll).optional.boolean().$;
if (pollErr) return rej('invalid poll param');
const query = {
createdAt: {
$gte: new Date(Date.now() - ms('1days'))
},
renoteCount: {
$gt: 0
}
} as any;
if (reply != undefined) {
query.replyId = reply ? { $exists: true, $ne: null } : null;
}
if (renote != undefined) {
query.renoteId = renote ? { $exists: true, $ne: null } : null;
}
if (media != undefined) {
query.mediaIds = media ? { $exists: true, $ne: null } : null;
}
if (poll != undefined) {
query.poll = poll ? { $exists: true, $ne: null } : null;
}
// Issue query
const notes = await Note
.find(query, {
limit: limit,
skip: offset,
sort: {
renoteCount: -1,
_id: -1
}
});
// Serialize
res(await Promise.all(notes.map(async note =>
await pack(note, user, { detail: true }))));
});