From 68571d8f577e0a9eb8679e8dd30e11d8b1709340 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 17 Apr 2021 15:30:26 +0900 Subject: [PATCH] Implement user online status Resolve #7422 Fix #7424 --- locales/ja-JP.yml | 6 ++++++ .../1618637372000-user-last-active-date.ts | 16 ++++++++++++++++ .../1618639857000-user-hide-online-status.ts | 14 ++++++++++++++ src/client/pages/settings/privacy.vue | 7 +++++++ src/const.ts | 2 ++ src/models/entities/user.ts | 11 +++++++++++ src/models/repositories/user.ts | 14 ++++++++++++++ .../api/endpoints/get-online-users-count.ts | 19 ++++++++++--------- src/server/api/endpoints/i/update.ts | 5 +++++ src/server/api/streaming.ts | 7 +++++++ 10 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 migration/1618637372000-user-last-active-date.ts create mode 100644 migration/1618639857000-user-hide-online-status.ts create mode 100644 src/const.ts diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 86deff6753..989edcda2d 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -722,6 +722,12 @@ notSpecifiedMentionWarning: "宛先に含まれていないメンションがあ info: "情報" userInfo: "ユーザー情報" unknown: "不明" +onlineStatus: "オンライン状態" +hideOnlineStatus: "オンライン状態を隠す" +hideOnlineStatusDescription: "オンライン状態を隠すと、検索などの一部機能において利便性が低下することがあります。" +online: "オンライン" +active: "アクティブ" +offline: "オフライン" _email: _follow: diff --git a/migration/1618637372000-user-last-active-date.ts b/migration/1618637372000-user-last-active-date.ts new file mode 100644 index 0000000000..a66c433a3a --- /dev/null +++ b/migration/1618637372000-user-last-active-date.ts @@ -0,0 +1,16 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class userLastActiveDate1618637372000 implements MigrationInterface { + name = 'userLastActiveDate1618637372000' + + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query(`DROP INDEX "IDX_seoignmeoprigmkpodgrjmkpormg"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "lastActiveDate"`); + } + +} diff --git a/migration/1618639857000-user-hide-online-status.ts b/migration/1618639857000-user-hide-online-status.ts new file mode 100644 index 0000000000..d5d77f9837 --- /dev/null +++ b/migration/1618639857000-user-hide-online-status.ts @@ -0,0 +1,14 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class userHideOnlineStatus1618639857000 implements MigrationInterface { + name = 'userHideOnlineStatus1618639857000' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user" ADD "hideOnlineStatus" boolean NOT NULL DEFAULT false`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "hideOnlineStatus"`); + } + +} diff --git a/src/client/pages/settings/privacy.vue b/src/client/pages/settings/privacy.vue index 0542c527f9..c8df378410 100644 --- a/src/client/pages/settings/privacy.vue +++ b/src/client/pages/settings/privacy.vue @@ -5,6 +5,10 @@ {{ $ts.autoAcceptFollowed }} + + {{ $ts.hideOnlineStatus }} + + {{ $ts.noCrawle }} @@ -58,6 +62,7 @@ export default defineComponent({ autoAcceptFollowed: false, noCrawle: false, isExplorable: false, + hideOnlineStatus: false, } }, @@ -72,6 +77,7 @@ export default defineComponent({ this.autoAcceptFollowed = this.$i.autoAcceptFollowed; this.noCrawle = this.$i.noCrawle; this.isExplorable = this.$i.isExplorable; + this.hideOnlineStatus = this.$i.hideOnlineStatus; }, mounted() { @@ -85,6 +91,7 @@ export default defineComponent({ autoAcceptFollowed: !!this.autoAcceptFollowed, noCrawle: !!this.noCrawle, isExplorable: !!this.isExplorable, + hideOnlineStatus: !!this.hideOnlineStatus, }); } } diff --git a/src/const.ts b/src/const.ts new file mode 100644 index 0000000000..43f59f1e4f --- /dev/null +++ b/src/const.ts @@ -0,0 +1,2 @@ +export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min +export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days diff --git a/src/models/entities/user.ts b/src/models/entities/user.ts index 91fbe35d94..060ec06b9a 100644 --- a/src/models/entities/user.ts +++ b/src/models/entities/user.ts @@ -26,6 +26,17 @@ export class User { }) 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', { length: 128, comment: 'The username of the User.' diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts index a3b4c69f43..0d59ed2545 100644 --- a/src/models/repositories/user.ts +++ b/src/models/repositories/user.ts @@ -7,6 +7,7 @@ import { SchemaType } from '@/misc/schema'; import { awaitAll } from '../../prelude/await-all'; import { populateEmojis } from '@/misc/populate-emojis'; import { getAntennas } from '@/misc/antenna-cache'; +import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const'; export type PackedUser = SchemaType; @@ -145,6 +146,17 @@ export class UserRepository extends Repository { 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( src: User['id'] | User, me?: { id: User['id'] } | null | undefined, @@ -192,6 +204,7 @@ export class UserRepository extends Repository { themeColor: instance.themeColor, } : undefined) : undefined, emojis: populateEmojis(user.emojis, user.host), + onlineStatus: this.getOnlineStatus(user), ...(opts.detail ? { url: profile!.url, @@ -239,6 +252,7 @@ export class UserRepository extends Repository { autoAcceptFollowed: profile!.autoAcceptFollowed, noCrawle: profile!.noCrawle, isExplorable: user.isExplorable, + hideOnlineStatus: user.hideOnlineStatus, hasUnreadSpecifiedNotes: NoteUnreads.count({ where: { userId: user.id, isSpecified: true }, take: 1 diff --git a/src/server/api/endpoints/get-online-users-count.ts b/src/server/api/endpoints/get-online-users-count.ts index 150ac9e365..a13363055f 100644 --- a/src/server/api/endpoints/get-online-users-count.ts +++ b/src/server/api/endpoints/get-online-users-count.ts @@ -1,6 +1,7 @@ +import { USER_ONLINE_THRESHOLD } from '@/const'; +import { Users } from '@/models'; +import { MoreThan } from 'typeorm'; import define from '../define'; -import { redisClient } from '../../../db/redis'; -import config from '@/config'; export const meta = { tags: ['meta'], @@ -11,12 +12,12 @@ export const meta = { } }; -export default define(meta, (ps, user) => { - return new Promise((res, rej) => { - redisClient.pubsub('numsub', config.host, (_, x) => { - res({ - count: x[1] - }); - }); +export default define(meta, async () => { + const count = await Users.count({ + lastActiveDate: MoreThan(new Date(Date.now() - USER_ONLINE_THRESHOLD)) }); + + return { + count + }; }); diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index c0ffd75e23..032dccd91a 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -96,6 +96,10 @@ export const meta = { validator: $.optional.bool, }, + hideOnlineStatus: { + validator: $.optional.bool, + }, + carefulBot: { validator: $.optional.bool, 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 (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked; 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.carefulBot === 'boolean') profileUpdates.carefulBot = ps.carefulBot; if (typeof ps.autoAcceptFollowed === 'boolean') profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts index 81b83edcf5..7224c23570 100644 --- a/src/server/api/streaming.ts +++ b/src/server/api/streaming.ts @@ -6,6 +6,7 @@ import { ParsedUrlQuery } from 'querystring'; import authenticate from './authenticate'; import { EventEmitter } from 'events'; import { subsdcriber as redisClient } from '../../db/redis'; +import { Users } from '@/models'; module.exports = (server: http.Server) => { // Init websocket server @@ -45,5 +46,11 @@ module.exports = (server: http.Server) => { connection.send('pong'); } }); + + if (user) { + Users.update(user.id, { + lastActiveDate: new Date(), + }); + } }); };