parent
							
								
									61461b7f59
								
							
						
					
					
						commit
						68571d8f57
					
				
					 10 changed files with 92 additions and 9 deletions
				
			
		|  | @ -722,6 +722,12 @@ notSpecifiedMentionWarning: "宛先に含まれていないメンションがあ | ||||||
| info: "情報" | info: "情報" | ||||||
| userInfo: "ユーザー情報" | userInfo: "ユーザー情報" | ||||||
| unknown: "不明" | unknown: "不明" | ||||||
|  | onlineStatus: "オンライン状態" | ||||||
|  | hideOnlineStatus: "オンライン状態を隠す" | ||||||
|  | hideOnlineStatusDescription: "オンライン状態を隠すと、検索などの一部機能において利便性が低下することがあります。" | ||||||
|  | online: "オンライン" | ||||||
|  | active: "アクティブ" | ||||||
|  | offline: "オフライン" | ||||||
| 
 | 
 | ||||||
| _email: | _email: | ||||||
|   _follow: |   _follow: | ||||||
|  |  | ||||||
							
								
								
									
										16
									
								
								migration/1618637372000-user-last-active-date.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								migration/1618637372000-user-last-active-date.ts
									
										
									
									
									
										Normal 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"`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										14
									
								
								migration/1618639857000-user-hide-online-status.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								migration/1618639857000-user-hide-online-status.ts
									
										
									
									
									
										Normal 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"`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -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
									
								
							
							
						
						
									
										2
									
								
								src/const.ts
									
										
									
									
									
										Normal 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
 | ||||||
|  | @ -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.' | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  | 	}; | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -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; | ||||||
|  |  | ||||||
|  | @ -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(), | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
| 	}); | 	}); | ||||||
| }; | }; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue