Implement user online status

Resolve #7422
Fix #7424
This commit is contained in:
syuilo 2021-04-17 15:30:26 +09:00
parent 61461b7f59
commit 68571d8f57
10 changed files with 92 additions and 9 deletions

View File

@ -722,6 +722,12 @@ notSpecifiedMentionWarning: "宛先に含まれていないメンションがあ
info: "情報" info: "情報"
userInfo: "ユーザー情報" userInfo: "ユーザー情報"
unknown: "不明" unknown: "不明"
onlineStatus: "オンライン状態"
hideOnlineStatus: "オンライン状態を隠す"
hideOnlineStatusDescription: "オンライン状態を隠すと、検索などの一部機能において利便性が低下することがあります。"
online: "オンライン"
active: "アクティブ"
offline: "オフライン"
_email: _email:
_follow: _follow:

View File

@ -0,0 +1,16 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class userLastActiveDate1618637372000 implements MigrationInterface {
name = 'userLastActiveDate1618637372000'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" ADD "lastActiveDate" TIMESTAMP WITH TIME ZONE DEFAULT NULL`);
await queryRunner.query(`CREATE INDEX "IDX_seoignmeoprigmkpodgrjmkpormg" ON "user" ("lastActiveDate") `);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_seoignmeoprigmkpodgrjmkpormg"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "lastActiveDate"`);
}
}

View File

@ -0,0 +1,14 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class userHideOnlineStatus1618639857000 implements MigrationInterface {
name = 'userHideOnlineStatus1618639857000'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" ADD "hideOnlineStatus" boolean NOT NULL DEFAULT false`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "hideOnlineStatus"`);
}
}

View File

@ -5,6 +5,10 @@
<FormSwitch v-model:value="autoAcceptFollowed" :disabled="!isLocked" @update:value="save()">{{ $ts.autoAcceptFollowed }}</FormSwitch> <FormSwitch v-model:value="autoAcceptFollowed" :disabled="!isLocked" @update:value="save()">{{ $ts.autoAcceptFollowed }}</FormSwitch>
<template #caption>{{ $ts.lockedAccountInfo }}</template> <template #caption>{{ $ts.lockedAccountInfo }}</template>
</FormGroup> </FormGroup>
<FormSwitch v-model:value="hideOnlineStatus" @update:value="save()">
{{ $ts.hideOnlineStatus }}
<template #desc>{{ $ts.hideOnlineStatusDescription }}</template>
</FormSwitch>
<FormSwitch v-model:value="noCrawle" @update:value="save()"> <FormSwitch v-model:value="noCrawle" @update:value="save()">
{{ $ts.noCrawle }} {{ $ts.noCrawle }}
<template #desc>{{ $ts.noCrawleDescription }}</template> <template #desc>{{ $ts.noCrawleDescription }}</template>
@ -58,6 +62,7 @@ export default defineComponent({
autoAcceptFollowed: false, autoAcceptFollowed: false,
noCrawle: false, noCrawle: false,
isExplorable: false, isExplorable: false,
hideOnlineStatus: false,
} }
}, },
@ -72,6 +77,7 @@ export default defineComponent({
this.autoAcceptFollowed = this.$i.autoAcceptFollowed; this.autoAcceptFollowed = this.$i.autoAcceptFollowed;
this.noCrawle = this.$i.noCrawle; this.noCrawle = this.$i.noCrawle;
this.isExplorable = this.$i.isExplorable; this.isExplorable = this.$i.isExplorable;
this.hideOnlineStatus = this.$i.hideOnlineStatus;
}, },
mounted() { mounted() {
@ -85,6 +91,7 @@ export default defineComponent({
autoAcceptFollowed: !!this.autoAcceptFollowed, autoAcceptFollowed: !!this.autoAcceptFollowed,
noCrawle: !!this.noCrawle, noCrawle: !!this.noCrawle,
isExplorable: !!this.isExplorable, isExplorable: !!this.isExplorable,
hideOnlineStatus: !!this.hideOnlineStatus,
}); });
} }
} }

2
src/const.ts Normal file
View File

@ -0,0 +1,2 @@
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days

View File

@ -26,6 +26,17 @@ export class User {
}) })
public lastFetchedAt: Date | null; public lastFetchedAt: Date | null;
@Index()
@Column('timestamp with time zone', {
nullable: true
})
public lastActiveDate: Date | null;
@Column('boolean', {
default: false,
})
public hideOnlineStatus: boolean;
@Column('varchar', { @Column('varchar', {
length: 128, length: 128,
comment: 'The username of the User.' comment: 'The username of the User.'

View File

@ -7,6 +7,7 @@ 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'; import { getAntennas } from '@/misc/antenna-cache';
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const';
export type PackedUser = SchemaType<typeof packedUserSchema>; export type PackedUser = SchemaType<typeof packedUserSchema>;
@ -145,6 +146,17 @@ export class UserRepository extends Repository<User> {
return count > 0; return count > 0;
} }
public getOnlineStatus(user: User): string {
if (user.hideOnlineStatus == null) return 'unknown';
if (user.lastActiveDate == null) return 'unknown';
const elapsed = Date.now() - user.lastActiveDate.getTime();
return (
elapsed < USER_ONLINE_THRESHOLD ? 'online' :
elapsed < USER_ACTIVE_THRESHOLD ? 'active' :
'offline'
);
}
public async pack( public async pack(
src: User['id'] | User, src: User['id'] | User,
me?: { id: User['id'] } | null | undefined, me?: { id: User['id'] } | null | undefined,
@ -192,6 +204,7 @@ export class UserRepository extends Repository<User> {
themeColor: instance.themeColor, themeColor: instance.themeColor,
} : undefined) : undefined, } : undefined) : undefined,
emojis: populateEmojis(user.emojis, user.host), emojis: populateEmojis(user.emojis, user.host),
onlineStatus: this.getOnlineStatus(user),
...(opts.detail ? { ...(opts.detail ? {
url: profile!.url, url: profile!.url,
@ -239,6 +252,7 @@ export class UserRepository extends Repository<User> {
autoAcceptFollowed: profile!.autoAcceptFollowed, autoAcceptFollowed: profile!.autoAcceptFollowed,
noCrawle: profile!.noCrawle, noCrawle: profile!.noCrawle,
isExplorable: user.isExplorable, isExplorable: user.isExplorable,
hideOnlineStatus: user.hideOnlineStatus,
hasUnreadSpecifiedNotes: NoteUnreads.count({ hasUnreadSpecifiedNotes: NoteUnreads.count({
where: { userId: user.id, isSpecified: true }, where: { userId: user.id, isSpecified: true },
take: 1 take: 1

View File

@ -1,6 +1,7 @@
import { USER_ONLINE_THRESHOLD } from '@/const';
import { Users } from '@/models';
import { MoreThan } from 'typeorm';
import define from '../define'; import define from '../define';
import { redisClient } from '../../../db/redis';
import config from '@/config';
export const meta = { export const meta = {
tags: ['meta'], tags: ['meta'],
@ -11,12 +12,12 @@ export const meta = {
} }
}; };
export default define(meta, (ps, user) => { export default define(meta, async () => {
return new Promise((res, rej) => { const count = await Users.count({
redisClient.pubsub('numsub', config.host, (_, x) => { lastActiveDate: MoreThan(new Date(Date.now() - USER_ONLINE_THRESHOLD))
res({
count: x[1]
});
});
}); });
return {
count
};
}); });

View File

@ -96,6 +96,10 @@ export const meta = {
validator: $.optional.bool, validator: $.optional.bool,
}, },
hideOnlineStatus: {
validator: $.optional.bool,
},
carefulBot: { carefulBot: {
validator: $.optional.bool, validator: $.optional.bool,
desc: { desc: {
@ -228,6 +232,7 @@ export default define(meta, async (ps, _user, token) => {
if (ps.mutingNotificationTypes !== undefined) profileUpdates.mutingNotificationTypes = ps.mutingNotificationTypes as typeof notificationTypes[number][]; if (ps.mutingNotificationTypes !== undefined) profileUpdates.mutingNotificationTypes = ps.mutingNotificationTypes as typeof notificationTypes[number][];
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable; if (typeof ps.isExplorable === 'boolean') updates.isExplorable = ps.isExplorable;
if (typeof ps.hideOnlineStatus === 'boolean') updates.hideOnlineStatus = ps.hideOnlineStatus;
if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot; if (typeof ps.isBot === 'boolean') updates.isBot = ps.isBot;
if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; if (typeof ps.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot;
if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;

View File

@ -6,6 +6,7 @@ import { ParsedUrlQuery } from 'querystring';
import authenticate from './authenticate'; import authenticate from './authenticate';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { subsdcriber as redisClient } from '../../db/redis'; import { subsdcriber as redisClient } from '../../db/redis';
import { Users } from '@/models';
module.exports = (server: http.Server) => { module.exports = (server: http.Server) => {
// Init websocket server // Init websocket server
@ -45,5 +46,11 @@ module.exports = (server: http.Server) => {
connection.send('pong'); connection.send('pong');
} }
}); });
if (user) {
Users.update(user.id, {
lastActiveDate: new Date(),
});
}
}); });
}; };