2017-09-07 19:13:01 +00:00
|
|
|
import * as mongo from 'mongodb';
|
2018-05-26 07:04:47 +00:00
|
|
|
import * as deepcopy from 'deepcopy';
|
2018-02-01 23:06:01 +00:00
|
|
|
import rap from '@prezzemolo/rap';
|
2018-03-29 11:32:18 +00:00
|
|
|
import db from '../db/mongodb';
|
2018-02-01 23:06:01 +00:00
|
|
|
import { IUser, pack as packUser } from './user';
|
|
|
|
import { pack as packApp } from './app';
|
2018-04-14 21:34:55 +00:00
|
|
|
import PollVote, { deletePollVote } from './poll-vote';
|
2018-04-11 18:46:32 +00:00
|
|
|
import Reaction, { deleteNoteReaction } from './note-reaction';
|
2018-02-01 23:06:01 +00:00
|
|
|
import { pack as packFile } from './drive-file';
|
2018-04-11 18:46:32 +00:00
|
|
|
import NoteWatching, { deleteNoteWatching } from './note-watching';
|
|
|
|
import NoteReaction from './note-reaction';
|
|
|
|
import Favorite, { deleteFavorite } from './favorite';
|
2018-04-14 21:34:55 +00:00
|
|
|
import Notification, { deleteNotification } from './notification';
|
2018-04-28 19:30:51 +00:00
|
|
|
import Following from './following';
|
2018-02-01 23:06:01 +00:00
|
|
|
|
2018-04-07 17:30:37 +00:00
|
|
|
const Note = db.get<INote>('notes');
|
|
|
|
Note.createIndex('uri', { sparse: true, unique: true });
|
2018-06-10 16:07:34 +00:00
|
|
|
Note.createIndex('userId');
|
|
|
|
Note.createIndex('tags', { sparse: true });
|
2018-06-08 19:14:26 +00:00
|
|
|
Note.createIndex({
|
|
|
|
createdAt: -1
|
|
|
|
});
|
2018-04-07 17:30:37 +00:00
|
|
|
export default Note;
|
2017-03-01 18:16:39 +00:00
|
|
|
|
|
|
|
export function isValidText(text: string): boolean {
|
|
|
|
return text.length <= 1000 && text.trim() != '';
|
|
|
|
}
|
2017-09-07 19:13:01 +00:00
|
|
|
|
2018-03-30 02:24:07 +00:00
|
|
|
export function isValidCw(text: string): boolean {
|
2018-04-22 08:04:52 +00:00
|
|
|
return text.length <= 100;
|
2018-03-30 02:24:07 +00:00
|
|
|
}
|
|
|
|
|
2018-04-07 17:30:37 +00:00
|
|
|
export type INote = {
|
2017-09-07 19:13:01 +00:00
|
|
|
_id: mongo.ObjectID;
|
2018-03-29 05:48:47 +00:00
|
|
|
createdAt: Date;
|
2018-04-06 22:19:30 +00:00
|
|
|
deletedAt: Date;
|
2018-03-29 05:48:47 +00:00
|
|
|
mediaIds: mongo.ObjectID[];
|
|
|
|
replyId: mongo.ObjectID;
|
2018-04-07 17:30:37 +00:00
|
|
|
renoteId: mongo.ObjectID;
|
2018-02-04 05:52:33 +00:00
|
|
|
poll: any; // todo
|
2017-09-07 19:13:01 +00:00
|
|
|
text: string;
|
2018-04-01 09:07:29 +00:00
|
|
|
tags: string[];
|
2018-03-30 02:24:07 +00:00
|
|
|
cw: string;
|
2018-03-29 05:48:47 +00:00
|
|
|
userId: mongo.ObjectID;
|
|
|
|
appId: mongo.ObjectID;
|
|
|
|
viaMobile: boolean;
|
2018-04-07 17:30:37 +00:00
|
|
|
renoteCount: number;
|
2018-03-29 05:48:47 +00:00
|
|
|
repliesCount: number;
|
|
|
|
reactionCounts: any;
|
|
|
|
mentions: mongo.ObjectID[];
|
2018-06-12 20:11:55 +00:00
|
|
|
mentionedRemoteUsers: Array<{
|
|
|
|
uri: string;
|
|
|
|
username: string;
|
|
|
|
host: string;
|
|
|
|
}>;
|
2018-04-28 08:25:56 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* public ... 公開
|
|
|
|
* home ... ホームタイムライン(ユーザーページのタイムライン含む)のみに流す
|
|
|
|
* followers ... フォロワーのみ
|
2018-04-28 19:30:51 +00:00
|
|
|
* specified ... visibleUserIds で指定したユーザーのみ
|
2018-04-28 08:25:56 +00:00
|
|
|
* private ... 自分のみ
|
|
|
|
*/
|
2018-04-28 19:30:51 +00:00
|
|
|
visibility: 'public' | 'home' | 'followers' | 'specified' | 'private';
|
|
|
|
|
|
|
|
visibleUserIds: mongo.ObjectID[];
|
2018-04-28 08:25:56 +00:00
|
|
|
|
2018-03-04 23:44:37 +00:00
|
|
|
geo: {
|
2018-03-29 06:23:15 +00:00
|
|
|
coordinates: number[];
|
2018-03-04 23:44:37 +00:00
|
|
|
altitude: number;
|
|
|
|
accuracy: number;
|
|
|
|
altitudeAccuracy: number;
|
|
|
|
heading: number;
|
|
|
|
speed: number;
|
|
|
|
};
|
2018-04-03 14:45:13 +00:00
|
|
|
uri: string;
|
2018-04-05 19:04:50 +00:00
|
|
|
|
2018-04-19 03:43:25 +00:00
|
|
|
// 非正規化
|
2018-04-05 19:04:50 +00:00
|
|
|
_reply?: {
|
|
|
|
userId: mongo.ObjectID;
|
|
|
|
};
|
2018-04-07 17:30:37 +00:00
|
|
|
_renote?: {
|
2018-04-05 19:04:50 +00:00
|
|
|
userId: mongo.ObjectID;
|
|
|
|
};
|
|
|
|
_user: {
|
|
|
|
host: string;
|
2018-04-19 03:43:25 +00:00
|
|
|
inbox?: string;
|
2018-04-05 19:04:50 +00:00
|
|
|
};
|
2018-05-28 15:36:52 +00:00
|
|
|
_replyIds?: mongo.ObjectID[];
|
2017-09-07 19:13:01 +00:00
|
|
|
};
|
2018-02-01 23:06:01 +00:00
|
|
|
|
2018-04-11 18:46:32 +00:00
|
|
|
/**
|
|
|
|
* Noteを物理削除します
|
|
|
|
*/
|
|
|
|
export async function deleteNote(note: string | mongo.ObjectID | INote) {
|
2018-04-11 09:24:42 +00:00
|
|
|
let n: INote;
|
|
|
|
|
|
|
|
// Populate
|
|
|
|
if (mongo.ObjectID.prototype.isPrototypeOf(note)) {
|
|
|
|
n = await Note.findOne({
|
|
|
|
_id: note
|
|
|
|
});
|
|
|
|
} else if (typeof note === 'string') {
|
|
|
|
n = await Note.findOne({
|
|
|
|
_id: new mongo.ObjectID(note)
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
n = note as INote;
|
|
|
|
}
|
|
|
|
|
2018-04-15 02:57:33 +00:00
|
|
|
console.log(n == null ? `Note: delete skipped ${note}` : `Note: deleting ${n._id}`);
|
|
|
|
|
2018-04-11 09:24:42 +00:00
|
|
|
if (n == null) return;
|
|
|
|
|
2018-04-11 18:46:32 +00:00
|
|
|
// このNoteへの返信をすべて削除
|
|
|
|
await Promise.all((
|
|
|
|
await Note.find({ replyId: n._id })
|
|
|
|
).map(x => deleteNote(x)));
|
2018-04-11 09:24:42 +00:00
|
|
|
|
2018-04-11 18:46:32 +00:00
|
|
|
// このNoteのRenoteをすべて削除
|
|
|
|
await Promise.all((
|
|
|
|
await Note.find({ renoteId: n._id })
|
|
|
|
).map(x => deleteNote(x)));
|
2018-04-11 09:24:42 +00:00
|
|
|
|
2018-04-11 18:46:32 +00:00
|
|
|
// この投稿に対するNoteWatchingをすべて削除
|
|
|
|
await Promise.all((
|
|
|
|
await NoteWatching.find({ noteId: n._id })
|
|
|
|
).map(x => deleteNoteWatching(x)));
|
|
|
|
|
|
|
|
// この投稿に対するNoteReactionをすべて削除
|
|
|
|
await Promise.all((
|
|
|
|
await NoteReaction.find({ noteId: n._id })
|
|
|
|
).map(x => deleteNoteReaction(x)));
|
2018-04-11 09:24:42 +00:00
|
|
|
|
2018-04-11 22:19:28 +00:00
|
|
|
// この投稿に対するPollVoteをすべて削除
|
|
|
|
await Promise.all((
|
|
|
|
await PollVote.find({ noteId: n._id })
|
|
|
|
).map(x => deletePollVote(x)));
|
|
|
|
|
2018-04-11 09:24:42 +00:00
|
|
|
// この投稿に対するFavoriteをすべて削除
|
2018-04-11 18:46:32 +00:00
|
|
|
await Promise.all((
|
|
|
|
await Favorite.find({ noteId: n._id })
|
|
|
|
).map(x => deleteFavorite(x)));
|
|
|
|
|
2018-04-14 21:34:55 +00:00
|
|
|
// この投稿に対するNotificationをすべて削除
|
|
|
|
await Promise.all((
|
|
|
|
await Notification.find({ noteId: n._id })
|
|
|
|
).map(x => deleteNotification(x)));
|
|
|
|
|
2018-04-11 18:46:32 +00:00
|
|
|
// このNoteを削除
|
|
|
|
await Note.remove({
|
|
|
|
_id: n._id
|
|
|
|
});
|
2018-04-15 02:57:33 +00:00
|
|
|
|
|
|
|
console.log(`Note: deleted ${n._id}`);
|
2018-04-11 09:24:42 +00:00
|
|
|
}
|
|
|
|
|
2018-02-01 23:06:01 +00:00
|
|
|
/**
|
2018-04-07 17:30:37 +00:00
|
|
|
* Pack a note for API response
|
2018-02-01 23:06:01 +00:00
|
|
|
*
|
2018-04-07 17:30:37 +00:00
|
|
|
* @param note target
|
2018-02-01 23:06:01 +00:00
|
|
|
* @param me? serializee
|
|
|
|
* @param options? serialize options
|
|
|
|
* @return response
|
|
|
|
*/
|
|
|
|
export const pack = async (
|
2018-04-07 17:30:37 +00:00
|
|
|
note: string | mongo.ObjectID | INote,
|
2018-02-01 23:06:01 +00:00
|
|
|
me?: string | mongo.ObjectID | IUser,
|
|
|
|
options?: {
|
|
|
|
detail: boolean
|
|
|
|
}
|
|
|
|
) => {
|
2018-04-28 22:01:47 +00:00
|
|
|
const opts = Object.assign({
|
|
|
|
detail: true
|
|
|
|
}, options);
|
2018-02-01 23:06:01 +00:00
|
|
|
|
|
|
|
// Me
|
|
|
|
const meId: mongo.ObjectID = me
|
|
|
|
? mongo.ObjectID.prototype.isPrototypeOf(me)
|
|
|
|
? me as mongo.ObjectID
|
|
|
|
: typeof me === 'string'
|
|
|
|
? new mongo.ObjectID(me)
|
|
|
|
: (me as IUser)._id
|
|
|
|
: null;
|
|
|
|
|
2018-04-07 17:30:37 +00:00
|
|
|
let _note: any;
|
2018-02-01 23:06:01 +00:00
|
|
|
|
2018-04-07 17:30:37 +00:00
|
|
|
// Populate the note if 'note' is ID
|
|
|
|
if (mongo.ObjectID.prototype.isPrototypeOf(note)) {
|
|
|
|
_note = await Note.findOne({
|
|
|
|
_id: note
|
2018-02-01 23:06:01 +00:00
|
|
|
});
|
2018-04-07 17:30:37 +00:00
|
|
|
} else if (typeof note === 'string') {
|
|
|
|
_note = await Note.findOne({
|
|
|
|
_id: new mongo.ObjectID(note)
|
2018-02-01 23:06:01 +00:00
|
|
|
});
|
|
|
|
} else {
|
2018-04-07 17:30:37 +00:00
|
|
|
_note = deepcopy(note);
|
2018-02-01 23:06:01 +00:00
|
|
|
}
|
|
|
|
|
2018-04-07 21:55:26 +00:00
|
|
|
if (!_note) throw `invalid note arg ${note}`;
|
2018-02-01 23:06:01 +00:00
|
|
|
|
2018-04-28 19:30:51 +00:00
|
|
|
let hide = false;
|
|
|
|
|
|
|
|
// visibility が private かつ投稿者のIDが自分のIDではなかったら非表示
|
|
|
|
if (_note.visibility == 'private' && (meId == null || !meId.equals(_note.userId))) {
|
|
|
|
hide = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// visibility が specified かつ自分が指定されていなかったら非表示
|
|
|
|
if (_note.visibility == 'specified') {
|
|
|
|
if (meId == null) {
|
|
|
|
hide = true;
|
|
|
|
} else if (meId.equals(_note.userId)) {
|
|
|
|
hide = false;
|
|
|
|
} else {
|
|
|
|
// 指定されているかどうか
|
2018-04-28 22:01:47 +00:00
|
|
|
const specified = _note.visibleUserIds.some(id => id.equals(meId));
|
2018-04-28 19:30:51 +00:00
|
|
|
|
|
|
|
if (specified) {
|
|
|
|
hide = false;
|
|
|
|
} else {
|
|
|
|
hide = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
|
|
|
|
if (_note.visibility == 'followers') {
|
|
|
|
if (meId == null) {
|
|
|
|
hide = true;
|
|
|
|
} else if (meId.equals(_note.userId)) {
|
|
|
|
hide = false;
|
|
|
|
} else {
|
|
|
|
// フォロワーかどうか
|
|
|
|
const following = await Following.findOne({
|
|
|
|
followeeId: _note.userId,
|
|
|
|
followerId: meId
|
|
|
|
});
|
|
|
|
|
|
|
|
if (following == null) {
|
|
|
|
hide = true;
|
|
|
|
} else {
|
|
|
|
hide = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-07 17:30:37 +00:00
|
|
|
const id = _note._id;
|
2018-02-01 23:06:01 +00:00
|
|
|
|
|
|
|
// Rename _id to id
|
2018-04-07 17:30:37 +00:00
|
|
|
_note.id = _note._id;
|
|
|
|
delete _note._id;
|
2018-02-01 23:06:01 +00:00
|
|
|
|
2018-04-28 22:01:47 +00:00
|
|
|
delete _note._user;
|
|
|
|
delete _note._reply;
|
|
|
|
delete _note.repost;
|
2018-04-07 17:30:37 +00:00
|
|
|
delete _note.mentions;
|
|
|
|
if (_note.geo) delete _note.geo.type;
|
2018-02-01 23:06:01 +00:00
|
|
|
|
|
|
|
// Populate user
|
2018-04-07 17:30:37 +00:00
|
|
|
_note.user = packUser(_note.userId, meId);
|
2018-02-01 23:06:01 +00:00
|
|
|
|
|
|
|
// Populate app
|
2018-04-07 17:30:37 +00:00
|
|
|
if (_note.appId) {
|
|
|
|
_note.app = packApp(_note.appId);
|
2018-02-01 23:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Populate media
|
2018-04-28 22:01:47 +00:00
|
|
|
_note.media = hide ? [] : Promise.all(_note.mediaIds.map(fileId =>
|
|
|
|
packFile(fileId)
|
|
|
|
));
|
2018-02-01 23:06:01 +00:00
|
|
|
|
2018-04-07 17:30:37 +00:00
|
|
|
// When requested a detailed note data
|
2018-02-01 23:06:01 +00:00
|
|
|
if (opts.detail) {
|
2018-05-14 00:01:37 +00:00
|
|
|
//#region 重いので廃止
|
|
|
|
_note.prev = null;
|
|
|
|
_note.next = null;
|
|
|
|
//#endregion
|
2018-02-01 23:06:01 +00:00
|
|
|
|
2018-04-07 17:30:37 +00:00
|
|
|
if (_note.replyId) {
|
|
|
|
// Populate reply to note
|
|
|
|
_note.reply = pack(_note.replyId, meId, {
|
2018-02-01 23:06:01 +00:00
|
|
|
detail: false
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-04-07 17:30:37 +00:00
|
|
|
if (_note.renoteId) {
|
|
|
|
// Populate renote
|
|
|
|
_note.renote = pack(_note.renoteId, meId, {
|
|
|
|
detail: _note.text == null
|
2018-02-01 23:06:01 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Poll
|
2018-04-28 19:51:19 +00:00
|
|
|
if (meId && _note.poll && !hide) {
|
2018-06-12 09:54:36 +00:00
|
|
|
_note.poll = (async poll => {
|
2018-04-14 21:34:55 +00:00
|
|
|
const vote = await PollVote
|
2018-02-01 23:06:01 +00:00
|
|
|
.findOne({
|
2018-03-29 05:48:47 +00:00
|
|
|
userId: meId,
|
2018-04-07 17:30:37 +00:00
|
|
|
noteId: id
|
2018-02-01 23:06:01 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
if (vote != null) {
|
|
|
|
const myChoice = poll.choices
|
|
|
|
.filter(c => c.id == vote.choice)[0];
|
|
|
|
|
2018-03-29 05:48:47 +00:00
|
|
|
myChoice.isVoted = true;
|
2018-02-01 23:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return poll;
|
2018-04-07 17:30:37 +00:00
|
|
|
})(_note.poll);
|
2018-02-01 23:06:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Fetch my reaction
|
|
|
|
if (meId) {
|
2018-04-07 17:30:37 +00:00
|
|
|
_note.myReaction = (async () => {
|
2018-02-01 23:06:01 +00:00
|
|
|
const reaction = await Reaction
|
|
|
|
.findOne({
|
2018-03-29 05:48:47 +00:00
|
|
|
userId: meId,
|
2018-04-07 17:30:37 +00:00
|
|
|
noteId: id,
|
2018-03-29 05:48:47 +00:00
|
|
|
deletedAt: { $exists: false }
|
2018-02-01 23:06:01 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
if (reaction) {
|
|
|
|
return reaction.reaction;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
})();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-07 17:30:37 +00:00
|
|
|
// resolve promises in _note object
|
|
|
|
_note = await rap(_note);
|
2018-02-01 23:06:01 +00:00
|
|
|
|
2018-05-21 02:08:08 +00:00
|
|
|
if (_note.user.isCat && _note.text) {
|
|
|
|
_note.text = _note.text.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ');
|
|
|
|
}
|
|
|
|
|
2018-04-28 19:51:19 +00:00
|
|
|
if (hide) {
|
|
|
|
_note.mediaIds = [];
|
|
|
|
_note.text = null;
|
|
|
|
_note.poll = null;
|
|
|
|
_note.isHidden = true;
|
|
|
|
}
|
|
|
|
|
2018-04-07 17:30:37 +00:00
|
|
|
return _note;
|
2018-02-01 23:06:01 +00:00
|
|
|
};
|