noteのread処理

This commit is contained in:
syuilo 2021-03-23 15:06:56 +09:00
parent 00bc097abb
commit 7e4a800352
10 changed files with 132 additions and 57 deletions

36
src/misc/antenna-cache.ts Normal file
View File

@ -0,0 +1,36 @@
import { Antennas } from '../models';
import { Antenna } from '../models/entities/antenna';
import { subsdcriber } from '../db/redis';
let antennasFetched = false;
let antennas: Antenna[] = [];
export async function getAntennas() {
if (!antennasFetched) {
antennas = await Antennas.find();
antennasFetched = true;
}
return antennas;
}
subsdcriber.on('message', async (_, data) => {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message;
switch (type) {
case 'antennaCreated':
antennas.push(body);
break;
case 'antennaUpdated':
antennas[antennas.findIndex(a => a.id === body.id)] = body;
break;
case 'antennaDeleted':
antennas = antennas.filter(a => a.id !== body.id);
break;
default:
break;
}
}
});

View File

@ -4,18 +4,24 @@ import { User } from '../models/entities/user';
import { UserListJoinings, UserGroupJoinings } from '../models'; import { UserListJoinings, UserGroupJoinings } from '../models';
import parseAcct from './acct/parse'; import parseAcct from './acct/parse';
import { getFullApAccount } from './convert-host'; import { getFullApAccount } from './convert-host';
import { PackedNote } from '../models/repositories/note';
export async function checkHitAntenna(antenna: Antenna, note: Note, noteUser: User, followers: User['id'][]): Promise<boolean> { /**
* noteUserFollowers / antennaUserFollowing
*/
export async function checkHitAntenna(antenna: Antenna, note: (Note | PackedNote), noteUser: { username: string; host: string | null; }, noteUserFollowers?: User['id'][], antennaUserFollowing?: User['id'][]): Promise<boolean> {
if (note.visibility === 'specified') return false; if (note.visibility === 'specified') return false;
if (note.visibility === 'followers') { if (note.visibility === 'followers') {
if (!followers.includes(antenna.userId)) return false; if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false;
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false;
} }
if (!antenna.withReplies && note.replyId != null) return false; if (!antenna.withReplies && note.replyId != null) return false;
if (antenna.src === 'home') { if (antenna.src === 'home') {
if (!followers.includes(antenna.userId)) return false; if (noteUserFollowers && !noteUserFollowers.includes(antenna.userId)) return false;
if (antennaUserFollowing && !antennaUserFollowing.includes(note.userId)) return false;
} else if (antenna.src === 'list') { } else if (antenna.src === 'list') {
const listUsers = (await UserListJoinings.find({ const listUsers = (await UserListJoinings.find({
userListId: antenna.userListId! userListId: antenna.userListId!
@ -75,7 +81,7 @@ export async function checkHitAntenna(antenna: Antenna, note: Note, noteUser: Us
} }
if (antenna.withFile) { if (antenna.withFile) {
if (note.fileIds.length === 0) return false; if (note.fileIds && note.fileIds.length === 0) return false;
} }
// TODO: eval expression // TODO: eval expression

View File

@ -6,6 +6,7 @@ import config from '../../config';
import { SchemaType } from '../../misc/schema'; import { SchemaType } from '../../misc/schema';
import { awaitAll } from '../../prelude/await-all'; import { awaitAll } from '../../prelude/await-all';
import { populateEmojis } from '../../misc/populate-emojis'; import { populateEmojis } from '../../misc/populate-emojis';
import { getAntennas } from '../../misc/antenna-cache';
export type PackedUser = SchemaType<typeof packedUserSchema>; export type PackedUser = SchemaType<typeof packedUserSchema>;
@ -97,10 +98,10 @@ export class UserRepository extends Repository<User> {
} }
public async getHasUnreadAntenna(userId: User['id']): Promise<boolean> { public async getHasUnreadAntenna(userId: User['id']): Promise<boolean> {
const antennas = await Antennas.find({ userId }); const myAntennas = (await getAntennas()).filter(a => a.userId === userId);
const unread = antennas.length > 0 ? await AntennaNotes.findOne({ const unread = myAntennas.length > 0 ? await AntennaNotes.findOne({
antennaId: In(antennas.map(x => x.id)), antennaId: In(myAntennas.map(x => x.id)),
read: false read: false
}) : null; }) : null;

View File

@ -4,6 +4,7 @@ import { genId } from '../../../../misc/gen-id';
import { Antennas, UserLists, UserGroupJoinings } from '../../../../models'; import { Antennas, UserLists, UserGroupJoinings } from '../../../../models';
import { ID } from '../../../../misc/cafy-id'; import { ID } from '../../../../misc/cafy-id';
import { ApiError } from '../../error'; import { ApiError } from '../../error';
import { publishInternalEvent } from '../../../../services/stream';
export const meta = { export const meta = {
desc: { desc: {
@ -108,7 +109,7 @@ export default define(meta, async (ps, user) => {
} }
} }
const antenna = await Antennas.save({ const antenna = await Antennas.insert({
id: genId(), id: genId(),
createdAt: new Date(), createdAt: new Date(),
userId: user.id, userId: user.id,
@ -123,7 +124,9 @@ export default define(meta, async (ps, user) => {
withReplies: ps.withReplies, withReplies: ps.withReplies,
withFile: ps.withFile, withFile: ps.withFile,
notify: ps.notify, notify: ps.notify,
}); }).then(x => Antennas.findOneOrFail(x.identifiers[0]));
publishInternalEvent('antennaCreated', antenna);
return await Antennas.pack(antenna); return await Antennas.pack(antenna);
}); });

View File

@ -3,6 +3,7 @@ import { ID } from '../../../../misc/cafy-id';
import define from '../../define'; import define from '../../define';
import { ApiError } from '../../error'; import { ApiError } from '../../error';
import { Antennas } from '../../../../models'; import { Antennas } from '../../../../models';
import { publishInternalEvent } from '../../../../services/stream';
export const meta = { export const meta = {
desc: { desc: {
@ -42,4 +43,6 @@ export default define(meta, async (ps, user) => {
} }
await Antennas.delete(antenna.id); await Antennas.delete(antenna.id);
publishInternalEvent('antennaDeleted', antenna);
}); });

View File

@ -3,6 +3,7 @@ import { ID } from '../../../../misc/cafy-id';
import define from '../../define'; import define from '../../define';
import { ApiError } from '../../error'; import { ApiError } from '../../error';
import { Antennas, UserLists, UserGroupJoinings } from '../../../../models'; import { Antennas, UserLists, UserGroupJoinings } from '../../../../models';
import { publishInternalEvent } from '../../../../services/stream';
export const meta = { export const meta = {
desc: { desc: {
@ -141,5 +142,7 @@ export default define(meta, async (ps, user) => {
notify: ps.notify, notify: ps.notify,
}); });
publishInternalEvent('antennaUpdated', Antennas.findOneOrFail(antenna.id));
return await Antennas.pack(antenna.id); return await Antennas.pack(antenna.id);
}); });

View File

@ -168,17 +168,10 @@ export default class Connection {
if (note == null) return; if (note == null) return;
if (this.user && (note.userId !== this.user.id)) { if (this.user && (note.userId !== this.user.id)) {
if (note.mentions && note.mentions.includes(this.user.id)) { readNote(this.user.id, [note], {
readNote(this.user.id, [note]); following: this.following,
} else if (note.visibleUserIds && note.visibleUserIds.includes(this.user.id)) { followingChannels: this.followingChannels,
readNote(this.user.id, [note]); });
}
if (this.followingChannels.has(note.channelId)) {
// TODO
}
// TODO: アンテナの既読処理
} }
} }

View File

@ -33,6 +33,7 @@ import { countSameRenotes } from '../../misc/count-same-renotes';
import { deliverToRelays } from '../relay'; import { deliverToRelays } from '../relay';
import { Channel } from '../../models/entities/channel'; import { Channel } from '../../models/entities/channel';
import { normalizeForSearch } from '../../misc/normalize-for-search'; import { normalizeForSearch } from '../../misc/normalize-for-search';
import { getAntennas } from '../../misc/antenna-cache';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -241,6 +242,7 @@ export default async (user: User, data: Option, silent = false) => new Promise<N
incNotesCountOfUser(user); incNotesCountOfUser(user);
// Word mute // Word mute
// TODO: cache
UserProfiles.find({ UserProfiles.find({
enableWordMute: true enableWordMute: true
}).then(us => { }).then(us => {
@ -262,17 +264,15 @@ export default async (user: User, data: Option, silent = false) => new Promise<N
Followings.createQueryBuilder('following') Followings.createQueryBuilder('following')
.andWhere(`following.followeeId = :userId`, { userId: note.userId }) .andWhere(`following.followeeId = :userId`, { userId: note.userId })
.getMany() .getMany()
.then(followings => { .then(async followings => {
const followers = followings.map(f => f.followerId); const followers = followings.map(f => f.followerId);
Antennas.find().then(async antennas => { for (const antenna of (await getAntennas())) {
for (const antenna of antennas) { checkHitAntenna(antenna, note, user, followers).then(hit => {
checkHitAntenna(antenna, note, user, followers).then(hit => { if (hit) {
if (hit) { addNoteToAntenna(antenna, note, user);
addNoteToAntenna(antenna, note, user); }
} });
}); }
}
});
}); });
// Channel // Channel

View File

@ -1,23 +1,59 @@
import { publishMainStream } from '../stream'; import { publishMainStream } from '../stream';
import { Note } from '../../models/entities/note'; import { Note } from '../../models/entities/note';
import { User } from '../../models/entities/user'; import { User } from '../../models/entities/user';
import { NoteUnreads, Antennas, AntennaNotes, Users } from '../../models'; import { NoteUnreads, AntennaNotes, Users } from '../../models';
import { Not, IsNull, In } from 'typeorm'; import { Not, IsNull, In } from 'typeorm';
import { Channel } from '../../models/entities/channel';
import { checkHitAntenna } from '../../misc/check-hit-antenna';
import { getAntennas } from '../../misc/antenna-cache';
import { PackedNote } from '../../models/repositories/note';
/** /**
* Mark notes as read * Mark notes as read
*/ */
export default async function( export default async function(
userId: User['id'], userId: User['id'],
noteIds: Note['id'][] notes: (Note | PackedNote)[],
info: {
following: Set<Channel['id']>;
followingChannels: Set<Channel['id']>;
}
) { ) {
async function careNoteUnreads() { const myAntennas = (await getAntennas()).filter(a => a.userId === userId);
const readMentions: (Note | PackedNote)[] = [];
const readSpecifiedNotes: (Note | PackedNote)[] = [];
const readChannelNotes: (Note | PackedNote)[] = [];
const readAntennaNotes: (Note | PackedNote)[] = [];
for (const note of notes) {
if (note.mentions && note.mentions.includes(userId)) {
readMentions.push(note);
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
readSpecifiedNotes.push(note);
}
if (note.channelId && info.followingChannels.has(note.channelId)) {
readChannelNotes.push(note);
}
if (note.user != null) { // たぶんnullになることは無いはずだけど一応
for (const antenna of myAntennas) {
if (checkHitAntenna(antenna, note, note.user as any, undefined, Array.from(info.following))) {
readAntennaNotes.push(note);
}
}
}
}
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0) || (readChannelNotes.length > 0)) {
// Remove the record // Remove the record
await NoteUnreads.delete({ await NoteUnreads.delete({
userId: userId, userId: userId,
noteId: In(noteIds), noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id), ...readChannelNotes.map(n => n.id)]),
}); });
// TODO: ↓まとめてクエリしたい
NoteUnreads.count({ NoteUnreads.count({
userId: userId, userId: userId,
isMentioned: true isMentioned: true
@ -49,33 +85,25 @@ export default async function(
}); });
} }
async function careAntenna() { if (readAntennaNotes.length > 0) {
const antennas = await Antennas.find({ userId }); await AntennaNotes.update({
antennaId: In(myAntennas.map(a => a.id)),
noteId: In(readAntennaNotes.map(n => n.id))
}, {
read: true
});
await Promise.all(antennas.map(async antenna => { // TODO: まとめてクエリしたい
const countBefore = await AntennaNotes.count({ for (const antenna of myAntennas) {
const count = await AntennaNotes.count({
antennaId: antenna.id, antennaId: antenna.id,
read: false read: false
}); });
if (countBefore === 0) return; if (count === 0) {
await AntennaNotes.update({
antennaId: antenna.id,
noteId: In(noteIds)
}, {
read: true
});
const countAfter = await AntennaNotes.count({
antennaId: antenna.id,
read: false
});
if (countAfter === 0) {
publishMainStream(userId, 'readAntenna', antenna); publishMainStream(userId, 'readAntenna', antenna);
} }
})); }
Users.getHasUnreadAntenna(userId).then(unread => { Users.getHasUnreadAntenna(userId).then(unread => {
if (!unread) { if (!unread) {
@ -83,7 +111,4 @@ export default async function(
} }
}); });
} }
careNoteUnreads();
careAntenna();
} }

View File

@ -20,6 +20,10 @@ class Publisher {
})); }));
} }
public publishInternalEvent = (type: string, value?: any): void => {
this.publish('internal', type, typeof value === 'undefined' ? null : value);
}
public publishUserEvent = (userId: User['id'], type: string, value?: any): void => { public publishUserEvent = (userId: User['id'], type: string, value?: any): void => {
this.publish(`user:${userId}`, type, typeof value === 'undefined' ? null : value); this.publish(`user:${userId}`, type, typeof value === 'undefined' ? null : value);
} }
@ -88,6 +92,7 @@ const publisher = new Publisher();
export default publisher; export default publisher;
export const publishInternalEvent = publisher.publishInternalEvent;
export const publishUserEvent = publisher.publishUserEvent; export const publishUserEvent = publisher.publishUserEvent;
export const publishBroadcastStream = publisher.publishBroadcastStream; export const publishBroadcastStream = publisher.publishBroadcastStream;
export const publishMainStream = publisher.publishMainStream; export const publishMainStream = publisher.publishMainStream;