noteのread処理
This commit is contained in:
parent
00bc097abb
commit
7e4a800352
10 changed files with 132 additions and 57 deletions
36
src/misc/antenna-cache.ts
Normal file
36
src/misc/antenna-cache.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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: アンテナの既読処理
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,10 +264,9 @@ 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);
|
||||||
|
@ -273,7 +274,6 @@ export default async (user: User, data: Option, silent = false) => new Promise<N
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Channel
|
// Channel
|
||||||
if (note.channelId) {
|
if (note.channelId) {
|
||||||
|
|
|
@ -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 Promise.all(antennas.map(async antenna => {
|
|
||||||
const countBefore = await AntennaNotes.count({
|
|
||||||
antennaId: antenna.id,
|
|
||||||
read: false
|
|
||||||
});
|
|
||||||
|
|
||||||
if (countBefore === 0) return;
|
|
||||||
|
|
||||||
await AntennaNotes.update({
|
await AntennaNotes.update({
|
||||||
antennaId: antenna.id,
|
antennaId: In(myAntennas.map(a => a.id)),
|
||||||
noteId: In(noteIds)
|
noteId: In(readAntennaNotes.map(n => n.id))
|
||||||
}, {
|
}, {
|
||||||
read: true
|
read: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const countAfter = await AntennaNotes.count({
|
// TODO: まとめてクエリしたい
|
||||||
|
for (const antenna of myAntennas) {
|
||||||
|
const count = await AntennaNotes.count({
|
||||||
antennaId: antenna.id,
|
antennaId: antenna.id,
|
||||||
read: false
|
read: false
|
||||||
});
|
});
|
||||||
|
|
||||||
if (countAfter === 0) {
|
if (count === 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();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue