diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2018-10-08 15:37:24 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2018-10-08 15:37:24 +0900 |
| commit | 9c170c426be01773afb15a9868ff3c278e09409c (patch) | |
| tree | 0229bb52dd9197308d193f4e41bbc11d3dcb95a1 /src/services/note | |
| parent | New translations ja-JP.yml (Norwegian) (diff) | |
| parent | fix(package): update @types/mongodb to version 3.1.10 (#2849) (diff) | |
| download | misskey-9c170c426be01773afb15a9868ff3c278e09409c.tar.gz misskey-9c170c426be01773afb15a9868ff3c278e09409c.tar.bz2 misskey-9c170c426be01773afb15a9868ff3c278e09409c.zip | |
Merge branch 'develop' into l10n_develop
Diffstat (limited to 'src/services/note')
| -rw-r--r-- | src/services/note/create.ts | 123 | ||||
| -rw-r--r-- | src/services/note/delete.ts | 18 | ||||
| -rw-r--r-- | src/services/note/reaction/create.ts | 4 | ||||
| -rw-r--r-- | src/services/note/read.ts | 66 | ||||
| -rw-r--r-- | src/services/note/unread.ts | 47 |
5 files changed, 210 insertions, 48 deletions
diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 63e3557828..3dc411d434 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -1,7 +1,7 @@ import es from '../../db/elasticsearch'; import Note, { pack, INote } from '../../models/note'; import User, { isLocalUser, IUser, isRemoteUser, IRemoteUser, ILocalUser } from '../../models/user'; -import { publishUserStream, publishLocalTimelineStream, publishHybridTimelineStream, publishGlobalTimelineStream, publishUserListStream } from '../../stream'; +import { publishMainStream, publishHomeTimelineStream, publishLocalTimelineStream, publishHybridTimelineStream, publishGlobalTimelineStream, publishUserListStream, publishHashtagStream } from '../../stream'; import Following from '../../models/following'; import { deliver } from '../../queue'; import renderNote from '../../remote/activitypub/renderer/note'; @@ -24,6 +24,8 @@ import isQuote from '../../misc/is-quote'; import { TextElementMention } from '../../mfm/parse/elements/mention'; import { TextElementHashtag } from '../../mfm/parse/elements/hashtag'; import { updateNoteStats } from '../update-chart'; +import { erase, unique } from '../../prelude/array'; +import insertNoteUnread from './unread'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -84,7 +86,7 @@ type Option = { text?: string; reply?: INote; renote?: INote; - media?: IDriveFile[]; + files?: IDriveFile[]; geo?: any; poll?: any; viaMobile?: boolean; @@ -103,23 +105,30 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< if (data.viaMobile == null) data.viaMobile = false; if (data.visibleUsers) { - data.visibleUsers = data.visibleUsers.filter(x => x != null); + data.visibleUsers = erase(null, data.visibleUsers); } + // リプライ対象が削除された投稿だったらreject if (data.reply && data.reply.deletedAt != null) { return rej(); } + // Renote対象が削除された投稿だったらreject if (data.renote && data.renote.deletedAt != null) { return rej(); } - // リプライ先が自分以外の非公開の投稿なら禁止 + // Renote対象が「ホームまたは全体」以外の公開範囲ならreject + if (data.renote && data.renote.visibility != 'public' && data.renote.visibility != 'home') { + return rej(); + } + + // リプライ対象が自分以外の非公開の投稿なら禁止 if (data.reply && data.reply.visibility == 'private' && !data.reply.userId.equals(user._id)) { return rej(); } - // Renote先が自分以外の非公開の投稿なら禁止 + // Renote対象が自分以外の非公開の投稿なら禁止 if (data.renote && data.renote.visibility == 'private' && !data.renote.userId.equals(user._id)) { return rej(); } @@ -135,7 +144,19 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< const mentionedUsers = await extractMentionedUsers(tokens); - const note = await insertNote(user, data, tokens, tags, mentionedUsers); + if (data.reply && !user._id.equals(data.reply.userId) && !mentionedUsers.some(u => u._id.equals(data.reply.userId))) { + mentionedUsers.push(await User.findOne({ _id: data.reply.userId })); + } + + if (data.visibility == 'specified') { + data.visibleUsers.forEach(u => { + if (!mentionedUsers.some(x => x._id.equals(u._id))) { + mentionedUsers.push(u); + } + }); + } + + const note = await insertNote(user, data, tags, mentionedUsers); res(note); @@ -155,6 +176,17 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< // Increment notes count (user) incNotesCountOfUser(user); + // 未読通知を作成 + if (data.visibility == 'specified') { + data.visibleUsers.forEach(u => { + insertNoteUnread(u, note, true); + }); + } else { + mentionedUsers.forEach(u => { + insertNoteUnread(u, note, false); + }); + } + if (data.reply) { saveReply(data.reply, note); } @@ -174,14 +206,18 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< noteObj.isFirstNote = true; } + if (tags.length > 0) { + publishHashtagStream(noteObj); + } + const nm = new NotificationManager(user, note); const nmRelatedPromises = []; - createMentionedEvents(mentionedUsers, noteObj, nm); + createMentionedEvents(mentionedUsers, note, nm); const noteActivity = await renderActivity(data, note); - if (isLocalUser(user)) { + if (isLocalUser(user) && note.visibility != 'private') { deliverNoteToMentionedRemoteUsers(mentionedUsers, user, noteActivity); } @@ -198,7 +234,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< // 通知 if (isLocalUser(data.reply._user)) { nm.push(data.reply.userId, 'reply'); - publishUserStream(data.reply.userId, 'reply', noteObj); + publishMainStream(data.reply.userId, 'reply', noteObj); } } @@ -221,7 +257,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< // Publish event if (!user._id.equals(data.renote.userId) && isLocalUser(data.renote._user)) { - publishUserStream(data.renote.userId, 'renote', noteObj); + publishMainStream(data.renote.userId, 'renote', noteObj); } } @@ -238,7 +274,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< }); async function renderActivity(data: Option, note: INote) { - const content = data.renote && data.text == null + const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length == 0) ? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote._id}`, note) : renderCreate(await renderNote(note, false), note); @@ -266,13 +302,15 @@ async function publish(user: IUser, note: INote, noteObj: any, reply: INote, ren } if (['private', 'followers', 'specified'].includes(note.visibility)) { - // Publish event to myself's stream - publishUserStream(note.userId, 'note', await pack(note, user, { + const detailPackedNote = await pack(note, user, { detail: true - })); + }); + // Publish event to myself's stream + publishHomeTimelineStream(note.userId, detailPackedNote); + publishHybridTimelineStream(note.userId, detailPackedNote); } else { // Publish event to myself's stream - publishUserStream(note.userId, 'note', noteObj); + publishHomeTimelineStream(note.userId, noteObj); // Publish note to local and hybrid timeline stream if (note.visibility != 'home') { @@ -281,6 +319,9 @@ async function publish(user: IUser, note: INote, noteObj: any, reply: INote, ren if (note.visibility == 'public') { publishHybridTimelineStream(null, noteObj); + } else { + // Publish event to myself's stream + publishHybridTimelineStream(note.userId, noteObj); } } } @@ -290,29 +331,19 @@ async function publish(user: IUser, note: INote, noteObj: any, reply: INote, ren publishGlobalTimelineStream(noteObj); } - if (note.visibility == 'specified') { - visibleUsers.forEach(async (u) => { - const n = await pack(note, u, { - detail: true - }); - publishUserStream(u._id, 'note', n); - publishHybridTimelineStream(u._id, n); - }); - } - if (['public', 'home', 'followers'].includes(note.visibility)) { // フォロワーに配信 - publishToFollowers(note, noteObj, user, noteActivity); + publishToFollowers(note, user, noteActivity); } // リストに配信 publishToUserLists(note, noteObj); } -async function insertNote(user: IUser, data: Option, tokens: ReturnType<typeof parse>, tags: string[], mentionedUsers: IUser[]) { +async function insertNote(user: IUser, data: Option, tags: string[], mentionedUsers: IUser[]) { const insert: any = { createdAt: data.createdAt, - mediaIds: data.media ? data.media.map(file => file._id) : [], + fileIds: data.files ? data.files.map(file => file._id) : [], replyId: data.reply ? data.reply._id : null, renoteId: data.renote ? data.renote._id : null, text: data.text, @@ -347,7 +378,8 @@ async function insertNote(user: IUser, data: Option, tokens: ReturnType<typeof p _user: { host: user.host, inbox: isRemoteUser(user) ? user.inbox : undefined - } + }, + _files: data.files ? data.files : [] }; if (data.uri != null) insert.uri = data.uri; @@ -383,7 +415,7 @@ function extractHashtags(tokens: ReturnType<typeof parse>): string[] { .map(t => (t as TextElementHashtag).hashtag) .filter(tag => tag.length <= 100); - return [...new Set(hashtags)]; + return unique(hashtags); } function index(note: INote) { @@ -439,7 +471,12 @@ async function publishToUserLists(note: INote, noteObj: any) { }); } -async function publishToFollowers(note: INote, noteObj: any, user: IUser, noteActivity: any) { +async function publishToFollowers(note: INote, user: IUser, noteActivity: any) { + const detailPackedNote = await pack(note, null, { + detail: true, + skipHide: true + }); + const followers = await Following.find({ followeeId: note.userId }); @@ -458,10 +495,10 @@ async function publishToFollowers(note: INote, noteObj: any, user: IUser, noteAc } // Publish event to followers stream - publishUserStream(following.followerId, 'note', noteObj); + publishHomeTimelineStream(following.followerId, detailPackedNote); if (isRemoteUser(user) || note.visibility != 'public') { - publishHybridTimelineStream(following.followerId, noteObj); + publishHybridTimelineStream(following.followerId, detailPackedNote); } } else { // フォロワーがリモートユーザーかつ投稿者がローカルユーザーなら投稿を配信 @@ -483,9 +520,13 @@ function deliverNoteToMentionedRemoteUsers(mentionedUsers: IUser[], user: ILocal }); } -function createMentionedEvents(mentionedUsers: IUser[], noteObj: any, nm: NotificationManager) { +function createMentionedEvents(mentionedUsers: IUser[], note: INote, nm: NotificationManager) { mentionedUsers.filter(u => isLocalUser(u)).forEach(async (u) => { - publishUserStream(u._id, 'mention', noteObj); + const detailPackedNote = await pack(note, u, { + detail: true + }); + + publishMainStream(u._id, 'mention', detailPackedNote); // Create notification nm.push(u._id, 'mention'); @@ -540,20 +581,20 @@ function incNotesCount(user: IUser) { async function extractMentionedUsers(tokens: ReturnType<typeof parse>): Promise<IUser[]> { if (tokens == null) return []; - const mentionTokens = [...new Set( + const mentionTokens = unique( tokens .filter(t => t.type == 'mention') as TextElementMention[] - )]; + ); - const mentionedUsers = [...new Set( - (await Promise.all(mentionTokens.map(async m => { + const mentionedUsers = unique( + erase(null, await Promise.all(mentionTokens.map(async m => { try { return await resolveUser(m.username, m.host); } catch (e) { return null; } - }))).filter(x => x != null) - )]; + }))) + ); return mentionedUsers; } diff --git a/src/services/note/delete.ts b/src/services/note/delete.ts index d0e2b12b41..2b99b4b85e 100644 --- a/src/services/note/delete.ts +++ b/src/services/note/delete.ts @@ -5,8 +5,9 @@ import renderDelete from '../../remote/activitypub/renderer/delete'; import pack from '../../remote/activitypub/renderer'; import { deliver } from '../../queue'; import Following from '../../models/following'; -import renderNote from '../../remote/activitypub/renderer/note'; +import renderTombstone from '../../remote/activitypub/renderer/tombstone'; import { updateNoteStats } from '../update-chart'; +import config from '../../config'; /** * 投稿を削除します。 @@ -14,25 +15,30 @@ import { updateNoteStats } from '../update-chart'; * @param note 投稿 */ export default async function(user: IUser, note: INote) { + const deletedAt = new Date(); + await Note.update({ _id: note._id, userId: user._id }, { $set: { - deletedAt: new Date(), + deletedAt: deletedAt, text: null, tags: [], - mediaIds: [], + fileIds: [], poll: null, - geo: null + geo: null, + cw: null } }); - publishNoteStream(note._id, 'deleted'); + publishNoteStream(note._id, 'deleted', { + deletedAt: deletedAt + }); //#region ローカルの投稿なら削除アクティビティを配送 if (isLocalUser(user)) { - const content = pack(renderDelete(await renderNote(note), user)); + const content = pack(renderDelete(renderTombstone(`${config.url}/notes/${note._id}`), user)); const followings = await Following.find({ followeeId: user._id, diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts index 5b6267b0dd..6884014e33 100644 --- a/src/services/note/reaction/create.ts +++ b/src/services/note/reaction/create.ts @@ -43,7 +43,9 @@ export default async (user: IUser, note: INote, reaction: string) => new Promise $inc: inc }); - publishNoteStream(note._id, 'reacted'); + publishNoteStream(note._id, 'reacted', { + reaction: reaction + }); // リアクションされたユーザーがローカルユーザーなら通知を作成 if (isLocalUser(note._user)) { diff --git a/src/services/note/read.ts b/src/services/note/read.ts new file mode 100644 index 0000000000..caf5cf318f --- /dev/null +++ b/src/services/note/read.ts @@ -0,0 +1,66 @@ +import * as mongo from 'mongodb'; +import { publishMainStream } from '../../stream'; +import User from '../../models/user'; +import NoteUnread from '../../models/note-unread'; + +/** + * Mark a note as read + */ +export default ( + user: string | mongo.ObjectID, + note: string | mongo.ObjectID +) => new Promise<any>(async (resolve, reject) => { + + const userId: mongo.ObjectID = mongo.ObjectID.prototype.isPrototypeOf(user) + ? user as mongo.ObjectID + : new mongo.ObjectID(user); + + const noteId: mongo.ObjectID = mongo.ObjectID.prototype.isPrototypeOf(note) + ? note as mongo.ObjectID + : new mongo.ObjectID(note); + + // Remove document + const res = await NoteUnread.remove({ + userId: userId, + noteId: noteId + }); + + if (res.deletedCount == 0) { + return; + } + + const count1 = await NoteUnread + .count({ + userId: userId, + isSpecified: false + }, { + limit: 1 + }); + + const count2 = await NoteUnread + .count({ + userId: userId, + isSpecified: true + }, { + limit: 1 + }); + + if (count1 == 0 || count2 == 0) { + User.update({ _id: userId }, { + $set: { + hasUnreadMentions: count1 != 0 || count2 != 0, + hasUnreadSpecifiedNotes: count2 != 0 + } + }); + } + + if (count1 == 0) { + // 全て既読になったイベントを発行 + publishMainStream(userId, 'readAllUnreadMentions'); + } + + if (count2 == 0) { + // 全て既読になったイベントを発行 + publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); + } +}); diff --git a/src/services/note/unread.ts b/src/services/note/unread.ts new file mode 100644 index 0000000000..e84ac2a4bf --- /dev/null +++ b/src/services/note/unread.ts @@ -0,0 +1,47 @@ +import NoteUnread from '../../models/note-unread'; +import User, { IUser } from '../../models/user'; +import { INote } from '../../models/note'; +import Mute from '../../models/mute'; +import { publishMainStream } from '../../stream'; + +export default async function(user: IUser, note: INote, isSpecified = false) { + //#region ミュートしているなら無視 + const mute = await Mute.find({ + muterId: user._id + }); + const mutedUserIds = mute.map(m => m.muteeId.toString()); + if (mutedUserIds.includes(note.userId.toString())) return; + //#endregion + + const unread = await NoteUnread.insert({ + noteId: note._id, + userId: user._id, + isSpecified, + _note: { + userId: note.userId + } + }); + + // 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する + setTimeout(async () => { + const exist = await NoteUnread.findOne({ _id: unread._id }); + if (exist == null) return; + + User.update({ + _id: user._id + }, { + $set: isSpecified ? { + hasUnreadSpecifiedNotes: true, + hasUnreadMentions: true + } : { + hasUnreadMentions: true + } + }); + + publishMainStream(user._id, 'unreadMention', note._id); + + if (isSpecified) { + publishMainStream(user._id, 'unreadSpecifiedNote', note._id); + } + }, 2000); +} |