2017-09-07 12:13:01 -07:00
|
|
|
import * as mongo from 'mongodb';
|
2019-02-01 02:56:16 -08:00
|
|
|
import * as deepcopy from 'deepcopy';
|
2018-02-01 15:06:01 -08:00
|
|
|
import rap from '@prezzemolo/rap';
|
2019-02-03 01:16:57 -08:00
|
|
|
import db, { dbLogger } from '../db/mongodb';
|
2018-10-15 19:38:09 -07:00
|
|
|
import isObjectId from '../misc/is-objectid';
|
2018-09-10 02:02:46 -07:00
|
|
|
import { length } from 'stringz';
|
2018-02-01 15:06:01 -08:00
|
|
|
import { IUser, pack as packUser } from './user';
|
|
|
|
import { pack as packApp } from './app';
|
2018-10-29 05:06:23 -07:00
|
|
|
import PollVote from './poll-vote';
|
|
|
|
import Reaction from './note-reaction';
|
2018-10-04 09:43:47 -07:00
|
|
|
import { packMany as packFileMany, IDriveFile } from './drive-file';
|
2018-04-28 12:30:51 -07:00
|
|
|
import Following from './following';
|
2018-11-03 11:32:20 -07:00
|
|
|
import Emoji from './emoji';
|
2019-02-04 10:01:36 -08:00
|
|
|
import wrapUrl from '../misc/wrap-url';
|
2018-02-01 15:06:01 -08:00
|
|
|
|
2018-04-07 10:30:37 -07:00
|
|
|
const Note = db.get<INote>('notes');
|
|
|
|
Note.createIndex('uri', { sparse: true, unique: true });
|
2018-06-10 09:07:34 -07:00
|
|
|
Note.createIndex('userId');
|
2018-09-16 06:48:57 -07:00
|
|
|
Note.createIndex('mentions');
|
|
|
|
Note.createIndex('visibleUserIds');
|
2018-11-23 06:12:28 -08:00
|
|
|
Note.createIndex('replyId');
|
2018-06-15 23:23:03 -07:00
|
|
|
Note.createIndex('tagsLower');
|
2018-10-24 15:04:15 -07:00
|
|
|
Note.createIndex('_user.host');
|
2018-09-25 05:09:38 -07:00
|
|
|
Note.createIndex('_files._id');
|
2018-09-05 03:32:46 -07:00
|
|
|
Note.createIndex('_files.contentType');
|
2018-10-24 15:04:15 -07:00
|
|
|
Note.createIndex({ createdAt: -1 });
|
|
|
|
Note.createIndex({ score: -1 }, { sparse: true });
|
2018-04-07 10:30:37 -07:00
|
|
|
export default Note;
|
2017-03-01 10:16:39 -08:00
|
|
|
|
2018-03-29 19:24:07 -07:00
|
|
|
export function isValidCw(text: string): boolean {
|
2018-09-10 02:02:46 -07:00
|
|
|
return length(text.trim()) <= 100;
|
2018-03-29 19:24:07 -07:00
|
|
|
}
|
|
|
|
|
2018-04-07 10:30:37 -07:00
|
|
|
export type INote = {
|
2017-09-07 12:13:01 -07:00
|
|
|
_id: mongo.ObjectID;
|
2018-03-28 22:48:47 -07:00
|
|
|
createdAt: Date;
|
2018-04-06 15:19:30 -07:00
|
|
|
deletedAt: Date;
|
2018-09-05 03:32:46 -07:00
|
|
|
fileIds: mongo.ObjectID[];
|
2018-03-28 22:48:47 -07:00
|
|
|
replyId: mongo.ObjectID;
|
2018-04-07 10:30:37 -07:00
|
|
|
renoteId: mongo.ObjectID;
|
2019-01-20 20:27:19 -08:00
|
|
|
poll: IPoll;
|
2017-09-07 12:13:01 -07:00
|
|
|
text: string;
|
2018-04-01 02:07:29 -07:00
|
|
|
tags: string[];
|
2018-06-15 23:23:03 -07:00
|
|
|
tagsLower: string[];
|
2018-11-03 11:32:20 -07:00
|
|
|
emojis: string[];
|
2018-03-29 19:24:07 -07:00
|
|
|
cw: string;
|
2018-03-28 22:48:47 -07:00
|
|
|
userId: mongo.ObjectID;
|
|
|
|
appId: mongo.ObjectID;
|
|
|
|
viaMobile: boolean;
|
2018-11-15 12:47:29 -08:00
|
|
|
localOnly: boolean;
|
2018-04-07 10:30:37 -07:00
|
|
|
renoteCount: number;
|
2018-03-28 22:48:47 -07:00
|
|
|
repliesCount: number;
|
|
|
|
reactionCounts: any;
|
|
|
|
mentions: mongo.ObjectID[];
|
2019-02-04 01:27:45 -08:00
|
|
|
mentionedRemoteUsers: {
|
2018-06-12 13:11:55 -07:00
|
|
|
uri: string;
|
|
|
|
username: string;
|
|
|
|
host: string;
|
2019-02-04 01:27:45 -08:00
|
|
|
}[];
|
2018-04-28 01:25:56 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* public ... 公開
|
|
|
|
* home ... ホームタイムライン(ユーザーページのタイムライン含む)のみに流す
|
|
|
|
* followers ... フォロワーのみ
|
2018-04-28 12:30:51 -07:00
|
|
|
* specified ... visibleUserIds で指定したユーザーのみ
|
2018-04-28 01:25:56 -07:00
|
|
|
*/
|
2018-12-28 09:55:46 -08:00
|
|
|
visibility: 'public' | 'home' | 'followers' | 'specified';
|
2018-04-28 12:30:51 -07:00
|
|
|
|
|
|
|
visibleUserIds: mongo.ObjectID[];
|
2018-04-28 01:25:56 -07:00
|
|
|
|
2018-03-04 15:44:37 -08:00
|
|
|
geo: {
|
2018-03-28 23:23:15 -07:00
|
|
|
coordinates: number[];
|
2018-03-04 15:44:37 -08:00
|
|
|
altitude: number;
|
|
|
|
accuracy: number;
|
|
|
|
altitudeAccuracy: number;
|
|
|
|
heading: number;
|
|
|
|
speed: number;
|
|
|
|
};
|
2018-10-24 15:04:15 -07:00
|
|
|
|
2018-04-03 07:45:13 -07:00
|
|
|
uri: string;
|
2018-04-05 12:04:50 -07:00
|
|
|
|
2018-10-24 15:04:15 -07:00
|
|
|
/**
|
|
|
|
* 人気の投稿度合いを表すスコア
|
|
|
|
*/
|
|
|
|
score: number;
|
|
|
|
|
2018-04-18 20:43:25 -07:00
|
|
|
// 非正規化
|
2018-04-05 12:04:50 -07:00
|
|
|
_reply?: {
|
|
|
|
userId: mongo.ObjectID;
|
|
|
|
};
|
2018-04-07 10:30:37 -07:00
|
|
|
_renote?: {
|
2018-04-05 12:04:50 -07:00
|
|
|
userId: mongo.ObjectID;
|
|
|
|
};
|
|
|
|
_user: {
|
|
|
|
host: string;
|
2018-04-18 20:43:25 -07:00
|
|
|
inbox?: string;
|
2018-04-05 12:04:50 -07:00
|
|
|
};
|
2018-09-05 03:32:46 -07:00
|
|
|
_files?: IDriveFile[];
|
2017-09-07 12:13:01 -07:00
|
|
|
};
|
2018-02-01 15:06:01 -08:00
|
|
|
|
2019-01-20 20:27:19 -08:00
|
|
|
export type IPoll = {
|
|
|
|
choices: IChoice[]
|
|
|
|
};
|
|
|
|
|
|
|
|
export type IChoice = {
|
|
|
|
id: number;
|
|
|
|
text: string;
|
|
|
|
votes: number;
|
|
|
|
};
|
|
|
|
|
2018-09-09 10:43:16 -07:00
|
|
|
export const hideNote = async (packedNote: any, meId: mongo.ObjectID) => {
|
|
|
|
let hide = false;
|
|
|
|
|
2018-12-28 09:55:46 -08:00
|
|
|
// visibility が private かつ投稿者のIDが自分のIDではなかったら非表示(後方互換性のため)
|
2018-09-09 10:43:16 -07:00
|
|
|
if (packedNote.visibility == 'private' && (meId == null || !meId.equals(packedNote.userId))) {
|
|
|
|
hide = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// visibility が specified かつ自分が指定されていなかったら非表示
|
|
|
|
if (packedNote.visibility == 'specified') {
|
|
|
|
if (meId == null) {
|
|
|
|
hide = true;
|
|
|
|
} else if (meId.equals(packedNote.userId)) {
|
|
|
|
hide = false;
|
|
|
|
} else {
|
|
|
|
// 指定されているかどうか
|
2018-09-17 10:13:42 -07:00
|
|
|
const specified = packedNote.visibleUserIds.some((id: any) => meId.equals(id));
|
2018-09-09 10:43:16 -07:00
|
|
|
|
|
|
|
if (specified) {
|
|
|
|
hide = false;
|
|
|
|
} else {
|
|
|
|
hide = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
|
|
|
|
if (packedNote.visibility == 'followers') {
|
|
|
|
if (meId == null) {
|
|
|
|
hide = true;
|
|
|
|
} else if (meId.equals(packedNote.userId)) {
|
|
|
|
hide = false;
|
2019-01-29 00:34:43 -08:00
|
|
|
} else if (packedNote.reply && meId.equals(packedNote.reply.userId)) {
|
|
|
|
// 自分の投稿に対するリプライ
|
|
|
|
hide = false;
|
|
|
|
} else if (packedNote.mentions && packedNote.mentions.some((id: any) => meId.equals(id))) {
|
|
|
|
// 自分へのメンション
|
|
|
|
hide = false;
|
2018-09-09 10:43:16 -07:00
|
|
|
} else {
|
|
|
|
// フォロワーかどうか
|
|
|
|
const following = await Following.findOne({
|
|
|
|
followeeId: packedNote.userId,
|
|
|
|
followerId: meId
|
|
|
|
});
|
|
|
|
|
|
|
|
if (following == null) {
|
|
|
|
hide = true;
|
|
|
|
} else {
|
|
|
|
hide = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (hide) {
|
|
|
|
packedNote.fileIds = [];
|
|
|
|
packedNote.files = [];
|
|
|
|
packedNote.text = null;
|
|
|
|
packedNote.poll = null;
|
|
|
|
packedNote.cw = null;
|
|
|
|
packedNote.tags = [];
|
|
|
|
packedNote.geo = null;
|
|
|
|
packedNote.isHidden = true;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2018-10-30 19:22:49 -07:00
|
|
|
export const packMany = (
|
2018-10-03 08:39:11 -07:00
|
|
|
notes: (string | mongo.ObjectID | INote)[],
|
|
|
|
me?: string | mongo.ObjectID | IUser,
|
|
|
|
options?: {
|
|
|
|
detail?: boolean;
|
|
|
|
skipHide?: boolean;
|
|
|
|
}
|
|
|
|
) => {
|
2018-10-30 19:22:49 -07:00
|
|
|
return Promise.all(notes.map(n => pack(n, me, options)));
|
2018-10-03 08:39:11 -07:00
|
|
|
};
|
|
|
|
|
2018-02-01 15:06:01 -08:00
|
|
|
/**
|
2018-04-07 10:30:37 -07:00
|
|
|
* Pack a note for API response
|
2018-02-01 15:06:01 -08:00
|
|
|
*
|
2018-04-07 10:30:37 -07:00
|
|
|
* @param note target
|
2018-02-01 15:06:01 -08:00
|
|
|
* @param me? serializee
|
|
|
|
* @param options? serialize options
|
|
|
|
* @return response
|
|
|
|
*/
|
|
|
|
export const pack = async (
|
2018-04-07 10:30:37 -07:00
|
|
|
note: string | mongo.ObjectID | INote,
|
2018-02-01 15:06:01 -08:00
|
|
|
me?: string | mongo.ObjectID | IUser,
|
|
|
|
options?: {
|
2018-09-09 10:43:16 -07:00
|
|
|
detail?: boolean;
|
|
|
|
skipHide?: boolean;
|
2018-02-01 15:06:01 -08:00
|
|
|
}
|
|
|
|
) => {
|
2018-04-28 15:01:47 -07:00
|
|
|
const opts = Object.assign({
|
2018-09-09 10:43:16 -07:00
|
|
|
detail: true,
|
|
|
|
skipHide: false
|
2018-04-28 15:01:47 -07:00
|
|
|
}, options);
|
2018-02-01 15:06:01 -08:00
|
|
|
|
|
|
|
// Me
|
|
|
|
const meId: mongo.ObjectID = me
|
2018-10-15 19:38:09 -07:00
|
|
|
? isObjectId(me)
|
2018-02-01 15:06:01 -08:00
|
|
|
? me as mongo.ObjectID
|
|
|
|
: typeof me === 'string'
|
|
|
|
? new mongo.ObjectID(me)
|
|
|
|
: (me as IUser)._id
|
|
|
|
: null;
|
|
|
|
|
2018-04-07 10:30:37 -07:00
|
|
|
let _note: any;
|
2018-02-01 15:06:01 -08:00
|
|
|
|
2018-04-07 10:30:37 -07:00
|
|
|
// Populate the note if 'note' is ID
|
2018-10-15 19:38:09 -07:00
|
|
|
if (isObjectId(note)) {
|
2018-04-07 10:30:37 -07:00
|
|
|
_note = await Note.findOne({
|
|
|
|
_id: note
|
2018-02-01 15:06:01 -08:00
|
|
|
});
|
2018-04-07 10:30:37 -07:00
|
|
|
} else if (typeof note === 'string') {
|
|
|
|
_note = await Note.findOne({
|
|
|
|
_id: new mongo.ObjectID(note)
|
2018-02-01 15:06:01 -08:00
|
|
|
});
|
|
|
|
} else {
|
2018-04-07 10:30:37 -07:00
|
|
|
_note = deepcopy(note);
|
2018-02-01 15:06:01 -08:00
|
|
|
}
|
|
|
|
|
2018-10-10 10:19:21 -07:00
|
|
|
// (データベースの欠損などで)投稿がデータベース上に見つからなかったとき
|
2018-10-03 08:39:11 -07:00
|
|
|
if (_note == null) {
|
2019-02-03 01:16:57 -08:00
|
|
|
dbLogger.warn(`[DAMAGED DB] (missing) pkg: note :: ${note}`);
|
2018-10-03 08:39:11 -07:00
|
|
|
return null;
|
|
|
|
}
|
2018-02-01 15:06:01 -08:00
|
|
|
|
2018-04-07 10:30:37 -07:00
|
|
|
const id = _note._id;
|
2018-02-01 15:06:01 -08:00
|
|
|
|
2018-11-01 16:59:40 -07:00
|
|
|
// _note._userを消す前か、_note.userを解決した後でないとホストがわからない
|
|
|
|
if (_note._user) {
|
2018-11-03 11:32:20 -07:00
|
|
|
const host = _note._user.host;
|
|
|
|
// 互換性のため。(古いMisskeyではNoteにemojisが無い)
|
|
|
|
if (_note.emojis == null) {
|
|
|
|
_note.emojis = Emoji.find({
|
|
|
|
host: host
|
|
|
|
}, {
|
|
|
|
fields: { _id: false }
|
|
|
|
});
|
|
|
|
} else {
|
2019-02-04 10:01:36 -08:00
|
|
|
_note.emojis = (await Emoji.find({
|
2018-11-03 11:32:20 -07:00
|
|
|
name: { $in: _note.emojis },
|
|
|
|
host: host
|
|
|
|
}, {
|
|
|
|
fields: { _id: false }
|
2019-02-04 10:01:36 -08:00
|
|
|
})).map(emoji => async () => {
|
|
|
|
emoji.url = await wrapUrl(emoji.url, me);
|
|
|
|
return emoji;
|
2018-11-03 11:32:20 -07:00
|
|
|
});
|
|
|
|
}
|
2018-11-01 16:59:40 -07:00
|
|
|
}
|
|
|
|
|
2018-02-01 15:06:01 -08:00
|
|
|
// Rename _id to id
|
2018-04-07 10:30:37 -07:00
|
|
|
_note.id = _note._id;
|
|
|
|
delete _note._id;
|
2018-02-01 15:06:01 -08:00
|
|
|
|
2018-10-07 09:25:34 -07:00
|
|
|
delete _note.prev;
|
|
|
|
delete _note.next;
|
|
|
|
delete _note.tagsLower;
|
2018-10-24 15:04:15 -07:00
|
|
|
delete _note.score;
|
2018-04-28 15:01:47 -07:00
|
|
|
delete _note._user;
|
|
|
|
delete _note._reply;
|
2018-09-18 22:18:34 -07:00
|
|
|
delete _note._renote;
|
|
|
|
delete _note._files;
|
2018-11-23 06:12:28 -08:00
|
|
|
delete _note._replyIds;
|
2019-02-01 20:50:41 -08:00
|
|
|
delete _note.mentionedRemoteUsers;
|
2018-11-23 06:12:28 -08:00
|
|
|
|
2018-04-07 10:30:37 -07:00
|
|
|
if (_note.geo) delete _note.geo.type;
|
2018-02-01 15:06:01 -08:00
|
|
|
|
|
|
|
// Populate user
|
2019-02-04 10:01:36 -08:00
|
|
|
_note.user = packUser(_note.userId, me);
|
2018-02-01 15:06:01 -08:00
|
|
|
|
|
|
|
// Populate app
|
2018-04-07 10:30:37 -07:00
|
|
|
if (_note.appId) {
|
|
|
|
_note.app = packApp(_note.appId);
|
2018-02-01 15:06:01 -08:00
|
|
|
}
|
|
|
|
|
2018-09-05 03:32:46 -07:00
|
|
|
// Populate files
|
2019-02-04 10:01:36 -08:00
|
|
|
_note.files = packFileMany(_note.fileIds || [], { me });
|
2018-02-01 15:06:01 -08:00
|
|
|
|
2018-12-28 09:04:29 -08:00
|
|
|
// Some counts
|
|
|
|
_note.renoteCount = _note.renoteCount || 0;
|
|
|
|
_note.repliesCount = _note.repliesCount || 0;
|
|
|
|
_note.reactionCounts = _note.reactionCounts || {};
|
|
|
|
|
2018-09-05 03:32:46 -07:00
|
|
|
// 後方互換性のため
|
|
|
|
_note.mediaIds = _note.fileIds;
|
|
|
|
_note.media = _note.files;
|
|
|
|
|
2018-04-07 10:30:37 -07:00
|
|
|
// When requested a detailed note data
|
2018-02-01 15:06:01 -08:00
|
|
|
if (opts.detail) {
|
2018-04-07 10:30:37 -07:00
|
|
|
if (_note.replyId) {
|
|
|
|
// Populate reply to note
|
|
|
|
_note.reply = pack(_note.replyId, meId, {
|
2018-02-01 15:06:01 -08:00
|
|
|
detail: false
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-04-07 10:30:37 -07:00
|
|
|
if (_note.renoteId) {
|
|
|
|
// Populate renote
|
|
|
|
_note.renote = pack(_note.renoteId, meId, {
|
|
|
|
detail: _note.text == null
|
2018-02-01 15:06:01 -08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Poll
|
2018-09-09 10:43:16 -07:00
|
|
|
if (meId && _note.poll) {
|
2018-06-12 02:54:36 -07:00
|
|
|
_note.poll = (async poll => {
|
2018-04-14 14:34:55 -07:00
|
|
|
const vote = await PollVote
|
2018-02-01 15:06:01 -08:00
|
|
|
.findOne({
|
2018-03-28 22:48:47 -07:00
|
|
|
userId: meId,
|
2018-04-07 10:30:37 -07:00
|
|
|
noteId: id
|
2018-02-01 15:06:01 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
if (vote != null) {
|
|
|
|
const myChoice = poll.choices
|
2018-06-17 17:54:53 -07:00
|
|
|
.filter((c: any) => c.id == vote.choice)[0];
|
2018-02-01 15:06:01 -08:00
|
|
|
|
2018-03-28 22:48:47 -07:00
|
|
|
myChoice.isVoted = true;
|
2018-02-01 15:06:01 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
return poll;
|
2018-04-07 10:30:37 -07:00
|
|
|
})(_note.poll);
|
2018-02-01 15:06:01 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (meId) {
|
2018-10-12 08:54:30 -07:00
|
|
|
// Fetch my reaction
|
2018-04-07 10:30:37 -07:00
|
|
|
_note.myReaction = (async () => {
|
2018-02-01 15:06:01 -08:00
|
|
|
const reaction = await Reaction
|
|
|
|
.findOne({
|
2018-03-28 22:48:47 -07:00
|
|
|
userId: meId,
|
2018-04-07 10:30:37 -07:00
|
|
|
noteId: id,
|
2018-03-28 22:48:47 -07:00
|
|
|
deletedAt: { $exists: false }
|
2018-02-01 15:06:01 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
if (reaction) {
|
|
|
|
return reaction.reaction;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
})();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-07 10:30:37 -07:00
|
|
|
// resolve promises in _note object
|
|
|
|
_note = await rap(_note);
|
2018-02-01 15:06:01 -08:00
|
|
|
|
2018-10-10 10:11:12 -07:00
|
|
|
//#region (データベースの欠損などで)参照しているデータがデータベース上に見つからなかったとき
|
2018-10-04 09:43:47 -07:00
|
|
|
if (_note.user == null) {
|
2019-02-03 01:16:57 -08:00
|
|
|
dbLogger.warn(`[DAMAGED DB] (missing) pkg: note -> user :: ${_note.id} (user ${_note.userId})`);
|
2018-10-04 09:43:47 -07:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-10-11 01:00:22 -07:00
|
|
|
if (opts.detail) {
|
|
|
|
if (_note.replyId != null && _note.reply == null) {
|
2019-02-03 01:16:57 -08:00
|
|
|
dbLogger.warn(`[DAMAGED DB] (missing) pkg: note -> reply :: ${_note.id} (reply ${_note.replyId})`);
|
2018-10-11 01:00:22 -07:00
|
|
|
return null;
|
|
|
|
}
|
2018-10-10 10:11:12 -07:00
|
|
|
|
2018-10-11 01:00:22 -07:00
|
|
|
if (_note.renoteId != null && _note.renote == null) {
|
2019-02-03 01:16:57 -08:00
|
|
|
dbLogger.warn(`[DAMAGED DB] (missing) pkg: note -> renote :: ${_note.id} (renote ${_note.renoteId})`);
|
2018-10-11 01:00:22 -07:00
|
|
|
return null;
|
|
|
|
}
|
2018-10-10 10:11:12 -07:00
|
|
|
}
|
|
|
|
//#endregion
|
|
|
|
|
2018-05-20 19:08:08 -07:00
|
|
|
if (_note.user.isCat && _note.text) {
|
2019-01-02 15:38:26 -08:00
|
|
|
_note.text = (_note.text
|
|
|
|
// ja-JP
|
|
|
|
.replace(/な/g, 'にゃ').replace(/ナ/g, 'ニャ').replace(/ナ/g, 'ニャ')
|
|
|
|
// ko-KR
|
|
|
|
.replace(/[나-낳]/g, (match: string) => String.fromCharCode(
|
|
|
|
match.codePointAt(0) + '냐'.charCodeAt(0) - '나'.charCodeAt(0)
|
|
|
|
))
|
|
|
|
);
|
2018-05-20 19:08:08 -07:00
|
|
|
}
|
|
|
|
|
2018-09-09 10:43:16 -07:00
|
|
|
if (!opts.skipHide) {
|
|
|
|
await hideNote(_note, meId);
|
2018-04-28 12:51:19 -07:00
|
|
|
}
|
|
|
|
|
2018-04-07 10:30:37 -07:00
|
|
|
return _note;
|
2018-02-01 15:06:01 -08:00
|
|
|
};
|