summaryrefslogtreecommitdiff
path: root/src/services/note/create.ts
diff options
context:
space:
mode:
authorrinsuki <428rinsuki+git@gmail.com>2018-05-17 07:52:24 +0900
committerrinsuki <428rinsuki+git@gmail.com>2018-05-17 07:52:24 +0900
commit829b4012e6dc14eb64a3d8f60826fe9b6a41b40d (patch)
tree42ac37f323db349dca9316e6fdb39fc33b860686 /src/services/note/create.ts
parentadd yarn.lock to gitignore (diff)
parentUpdate deliver.ts (diff)
downloadmisskey-829b4012e6dc14eb64a3d8f60826fe9b6a41b40d.tar.gz
misskey-829b4012e6dc14eb64a3d8f60826fe9b6a41b40d.tar.bz2
misskey-829b4012e6dc14eb64a3d8f60826fe9b6a41b40d.zip
Merge branch 'master' into fix/yarn-lock-ignore
Diffstat (limited to 'src/services/note/create.ts')
-rw-r--r--src/services/note/create.ts367
1 files changed, 207 insertions, 160 deletions
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index e35e5ecfbd..f049c34b65 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -1,6 +1,6 @@
import Note, { pack, INote } from '../../models/note';
-import User, { isLocalUser, IUser, isRemoteUser } from '../../models/user';
-import stream, { publishLocalTimelineStream, publishGlobalTimelineStream } from '../../publishers/stream';
+import User, { isLocalUser, IUser, isRemoteUser, IRemoteUser, ILocalUser } from '../../models/user';
+import stream, { publishLocalTimelineStream, publishGlobalTimelineStream, publishUserListStream } from '../../publishers/stream';
import Following from '../../models/following';
import { deliver } from '../../queue';
import renderNote from '../../remote/activitypub/renderer/note';
@@ -15,8 +15,64 @@ import Mute from '../../models/mute';
import pushSw from '../../publishers/push-sw';
import event from '../../publishers/stream';
import parse from '../../text/parse';
-import html from '../../text/html';
import { IApp } from '../../models/app';
+import UserList from '../../models/user-list';
+import resolveUser from '../../remote/resolve-user';
+
+type Reason = 'reply' | 'quote' | 'mention';
+
+/**
+ * ServiceWorkerへの通知を担当
+ */
+class NotificationManager {
+ private user: IUser;
+ private note: any;
+ private list: Array<{
+ user: ILocalUser['_id'],
+ reason: Reason;
+ }> = [];
+
+ constructor(user, note) {
+ this.user = user;
+ this.note = note;
+ }
+
+ public push(user: ILocalUser['_id'], reason: Reason) {
+ // 自分自身へは通知しない
+ if (this.user._id.equals(user)) return;
+
+ const exist = this.list.find(x => x.user.equals(user));
+
+ if (exist) {
+ // 「メンションされているかつ返信されている」場合は、メンションとしての通知ではなく返信としての通知にする
+ if (reason != 'mention') {
+ exist.reason = reason;
+ }
+ } else {
+ this.list.push({
+ user, reason
+ });
+ }
+ }
+
+ public deliver() {
+ this.list.forEach(async x => {
+ const mentionee = x.user;
+
+ // ミュート情報を取得
+ const mentioneeMutes = await Mute.find({
+ muterId: mentionee
+ });
+
+ const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString());
+
+ // 通知される側のユーザーが通知する側のユーザーをミュートしていない限りは通知する
+ if (!mentioneesMutedUserIds.includes(this.user._id.toString())) {
+ pushSw(mentionee, x.reason, this.note);
+ }
+ });
+ }
+}
export default async (user: IUser, data: {
createdAt?: Date;
@@ -30,6 +86,7 @@ export default async (user: IUser, data: {
tags?: string[];
cw?: string;
visibility?: string;
+ visibleUsers?: IUser[];
uri?: string;
app?: IApp;
}, silent = false) => new Promise<INote>(async (res, rej) => {
@@ -39,7 +96,7 @@ export default async (user: IUser, data: {
const tags = data.tags || [];
- let tokens = null;
+ let tokens: any[] = null;
if (data.text) {
// Analyze
@@ -57,21 +114,29 @@ export default async (user: IUser, data: {
});
}
+ if (data.visibleUsers) {
+ data.visibleUsers = data.visibleUsers.filter(x => x != null);
+ }
+
const insert: any = {
createdAt: data.createdAt,
mediaIds: data.media ? data.media.map(file => file._id) : [],
replyId: data.reply ? data.reply._id : null,
renoteId: data.renote ? data.renote._id : null,
text: data.text,
- textHtml: tokens === null ? null : html(tokens),
poll: data.poll,
- cw: data.cw,
+ cw: data.cw == null ? null : data.cw,
tags,
userId: user._id,
viaMobile: data.viaMobile,
geo: data.geo || null,
appId: data.app ? data.app._id : null,
visibility: data.visibility,
+ visibleUserIds: data.visibility == 'specified'
+ ? data.visibleUsers
+ ? data.visibleUsers.map(u => u._id)
+ : []
+ : [],
// 以下非正規化データ
_reply: data.reply ? { userId: data.reply.userId } : null,
@@ -85,143 +150,167 @@ export default async (user: IUser, data: {
if (data.uri != null) insert.uri = data.uri;
// 投稿を作成
- const note = await Note.insert(insert);
+ let note: INote;
+ try {
+ note = await Note.insert(insert);
+ } catch (e) {
+ // duplicate key error
+ if (e.code === 11000) {
+ return res(null);
+ }
+
+ console.error(e);
+ return rej('something happened');
+ }
res(note);
+ // Increment notes count
User.update({ _id: user._id }, {
- // Increment notes count
$inc: {
notesCount: 1
- },
- // Update latest note
- $set: {
- latestNote: note
}
});
// Serialize
const noteObj = await pack(note);
- // タイムラインへの投稿
- if (note.channelId == null) {
+ const nm = new NotificationManager(user, noteObj);
+
+ const render = async () => {
+ const content = data.renote && data.text == null
+ ? renderAnnounce(data.renote.uri ? data.renote.uri : await renderNote(data.renote))
+ : renderCreate(await renderNote(note));
+ return packAp(content);
+ };
+
+ if (!silent) {
if (isLocalUser(user)) {
- // Publish event to myself's stream
- stream(note.userId, 'note', noteObj);
+ if (note.visibility == 'private' || note.visibility == 'followers' || note.visibility == 'specified') {
+ // Publish event to myself's stream
+ stream(note.userId, 'note', await pack(note, user, {
+ detail: true
+ }));
+ } else {
+ // Publish event to myself's stream
+ stream(note.userId, 'note', noteObj);
- // Publish note to local timeline stream
- publishLocalTimelineStream(noteObj);
+ // Publish note to local timeline stream
+ if (note.visibility != 'home') {
+ publishLocalTimelineStream(noteObj);
+ }
+ }
}
// Publish note to global timeline stream
publishGlobalTimelineStream(noteObj);
- // Fetch all followers
- const followers = await Following.aggregate([{
- $lookup: {
- from: 'users',
- localField: 'followerId',
- foreignField: '_id',
- as: 'user'
- }
- }, {
- $match: {
+ if (note.visibility == 'specified') {
+ data.visibleUsers.forEach(async u => {
+ stream(u._id, 'note', await pack(note, u, {
+ detail: true
+ }));
+ });
+ }
+
+ if (note.visibility == 'public' || note.visibility == 'home' || note.visibility == 'followers') {
+ // フォロワーに配信
+ Following.find({
followeeId: note.userId
- }
- }], {
- _id: false
- });
+ }).then(followers => {
+ followers.map(async following => {
+ const follower = following._follower;
- if (!silent) {
- const render = async () => {
- const content = data.renote && data.text == null
- ? renderAnnounce(data.renote.uri ? data.renote.uri : await renderNote(data.renote))
- : renderCreate(await renderNote(note));
- return packAp(content);
- };
+ if (isLocalUser(follower)) {
+ // ストーキングしていない場合
+ if (!following.stalk) {
+ // この投稿が返信ならスキップ
+ if (note.replyId && !note._reply.userId.equals(following.followerId) && !note._reply.userId.equals(note.userId)) return;
+ }
- // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
- if (data.reply && isLocalUser(user) && isRemoteUser(data.reply._user)) {
- deliver(user, await render(), data.reply._user.inbox);
- }
+ // Publish event to followers stream
+ stream(following.followerId, 'note', noteObj);
+ } else {
+ //#region AP配送
+ // フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信
+ if (isLocalUser(user)) {
+ deliver(user, await render(), follower.inbox);
+ }
+ //#endergion
+ }
+ });
+ });
+ }
- // 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送
- if (data.renote && isLocalUser(user) && isRemoteUser(data.renote._user)) {
- deliver(user, await render(), data.renote._user.inbox);
- }
+ // リストに配信
+ UserList.find({
+ userIds: note.userId
+ }).then(lists => {
+ lists.forEach(list => {
+ publishUserListStream(list._id, 'note', noteObj);
+ });
+ });
+ }
- Promise.all(followers.map(async follower => {
- follower = follower.user[0];
+ //#region リプライとAnnounceのAP配送
- if (isLocalUser(follower)) {
- // Publish event to followers stream
- stream(follower._id, 'note', noteObj);
- } else {
- // フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信
- if (isLocalUser(user)) {
- deliver(user, await render(), follower.inbox);
- }
- }
- }));
- }
+ // 投稿がリプライかつ投稿者がローカルユーザーかつリプライ先の投稿の投稿者がリモートユーザーなら配送
+ if (data.reply && isLocalUser(user) && isRemoteUser(data.reply._user)) {
+ deliver(user, await render(), data.reply._user.inbox);
}
- // チャンネルへの投稿
- /* TODO
- if (note.channelId) {
- promises.push(
- // Increment channel index(notes count)
- Channel.update({ _id: note.channelId }, {
- $inc: {
- index: 1
- }
- }),
+ // 投稿がRenoteかつ投稿者がローカルユーザーかつRenote元の投稿の投稿者がリモートユーザーなら配送
+ if (data.renote && isLocalUser(user) && isRemoteUser(data.renote._user)) {
+ deliver(user, await render(), data.renote._user.inbox);
+ }
+ //#endergion
- // Publish event to channel
- promisedNoteObj.then(noteObj => {
- publishChannelStream(note.channelId, 'note', noteObj);
- }),
+ //#region メンション
+ if (data.text) {
+ // TODO: Drop dupulicates
+ const mentions = tokens
+ .filter(t => t.type == 'mention');
- Promise.all([
- promisedNoteObj,
+ let mentionedUsers = await Promise.all(mentions.map(async m => {
+ try {
+ return await resolveUser(m.username, m.host);
+ } catch (e) {
+ return null;
+ }
+ }));
- // Get channel watchers
- ChannelWatching.find({
- channelId: note.channelId,
- // 削除されたドキュメントは除く
- deletedAt: { $exists: false }
- })
- ]).then(([noteObj, watches]) => {
- // チャンネルの視聴者(のタイムライン)に配信
- watches.forEach(w => {
- stream(w.userId, 'note', noteObj);
- });
- })
- );
- }*/
+ // TODO: Drop dupulicates
+ mentionedUsers = mentionedUsers.filter(x => x != null);
- const mentions = [];
+ mentionedUsers.filter(u => isLocalUser(u)).forEach(async u => {
+ // 既に言及されたユーザーに対する返信や引用renoteの場合も無視
+ if (data.reply && data.reply.userId.equals(u._id)) return;
+ if (data.renote && data.renote.userId.equals(u._id)) return;
- async function addMention(mentionee, reason) {
- // Reject if already added
- if (mentions.some(x => x.equals(mentionee))) return;
+ // Create notification
+ notify(u._id, user._id, 'mention', {
+ noteId: note._id
+ });
- // Add mention
- mentions.push(mentionee);
+ nm.push(u._id, 'mention');
+ });
- // Publish event
- if (!user._id.equals(mentionee)) {
- const mentioneeMutes = await Mute.find({
- muter_id: mentionee,
- deleted_at: { $exists: false }
+ if (isLocalUser(user)) {
+ mentionedUsers.filter(u => isRemoteUser(u)).forEach(async u => {
+ deliver(user, await render(), (u as IRemoteUser).inbox);
+ });
+ }
+
+ // Append mentions data
+ if (mentionedUsers.length > 0) {
+ Note.update({ _id: note._id }, {
+ $set: {
+ mentions: mentionedUsers.map(u => u._id)
+ }
});
- const mentioneesMutedUserIds = mentioneeMutes.map(m => m.muteeId.toString());
- if (mentioneesMutedUserIds.indexOf(user._id.toString()) == -1) {
- event(mentionee, reason, noteObj);
- pushSw(mentionee, reason, noteObj);
- }
}
}
+ //#endregion
// If has in reply to note
if (data.reply) {
@@ -260,8 +349,7 @@ export default async (user: IUser, data: {
watch(user._id, data.reply);
}
- // Add mention
- addMention(data.reply.userId, 'reply');
+ nm.push(data.reply.userId, 'reply');
}
// If it is renote
@@ -296,7 +384,7 @@ export default async (user: IUser, data: {
// If it is quote renote
if (data.text) {
// Add mention
- addMention(data.renote.userId, 'quote');
+ nm.push(data.renote.userId, 'quote');
} else {
// Publish event
if (!user._id.equals(data.renote.userId)) {
@@ -304,14 +392,17 @@ export default async (user: IUser, data: {
}
}
+ //#region TODO: これ重い
// 今までで同じ投稿をRenoteしているか
- const existRenote = await Note.findOne({
- userId: user._id,
- renoteId: data.renote._id,
- _id: {
- $ne: note._id
- }
- });
+ //const existRenote = await Note.findOne({
+ // userId: user._id,
+ // renoteId: data.renote._id,
+ // _id: {
+ // $ne: note._id
+ // }
+ //});
+ const existRenote = null;
+ //#endregion
if (!existRenote) {
// Update renoteee status
@@ -322,48 +413,4 @@ export default async (user: IUser, data: {
});
}
}
-
- // If has text content
- if (data.text) {
- // Extract an '@' mentions
- const atMentions = tokens
- .filter(t => t.type == 'mention')
- .map(m => m.username)
- // Drop dupulicates
- .filter((v, i, s) => s.indexOf(v) == i);
-
- // Resolve all mentions
- await Promise.all(atMentions.map(async mention => {
- // Fetch mentioned user
- // SELECT _id
- const mentionee = await User
- .findOne({
- usernameLower: mention.toLowerCase()
- }, { _id: true });
-
- // When mentioned user not found
- if (mentionee == null) return;
-
- // 既に言及されたユーザーに対する返信や引用renoteの場合も無視
- if (data.reply && data.reply.userId.equals(mentionee._id)) return;
- if (data.renote && data.renote.userId.equals(mentionee._id)) return;
-
- // Add mention
- addMention(mentionee._id, 'mention');
-
- // Create notification
- notify(mentionee._id, user._id, 'mention', {
- noteId: note._id
- });
- }));
- }
-
- // Append mentions data
- if (mentions.length > 0) {
- Note.update({ _id: note._id }, {
- $set: {
- mentions
- }
- });
- }
});