wip
This commit is contained in:
		
							
								
								
									
										17
									
								
								packages/backend/migration/1696331570827-hibernation.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								packages/backend/migration/1696331570827-hibernation.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
export class Hibernation1696331570827 {
 | 
			
		||||
    name = 'Hibernation1696331570827'
 | 
			
		||||
 | 
			
		||||
    async up(queryRunner) {
 | 
			
		||||
				await queryRunner.query(`DROP INDEX "public"."IDX_d74d8ab5efa7e3bb82825c0fa2"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "user" ADD "isHibernated" boolean NOT NULL DEFAULT false`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "following" ADD "isFollowerHibernated" boolean NOT NULL DEFAULT false`);
 | 
			
		||||
        await queryRunner.query(`CREATE INDEX "IDX_ce62b50d882d4e9dee10ad0d2f" ON "following" ("followeeId", "followerHost", "isFollowerHibernated") `);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async down(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`DROP INDEX "public"."IDX_ce62b50d882d4e9dee10ad0d2f"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "following" DROP COLUMN "isFollowerHibernated"`);
 | 
			
		||||
        await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isHibernated"`);
 | 
			
		||||
        await queryRunner.query(`CREATE INDEX "IDX_d74d8ab5efa7e3bb82825c0fa2" ON "following" ("followeeId", "followerHost") `);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -46,6 +46,7 @@ import { SignupService } from './SignupService.js';
 | 
			
		||||
import { WebAuthnService } from './WebAuthnService.js';
 | 
			
		||||
import { UserBlockingService } from './UserBlockingService.js';
 | 
			
		||||
import { CacheService } from './CacheService.js';
 | 
			
		||||
import { UserService } from './UserService.js';
 | 
			
		||||
import { UserFollowingService } from './UserFollowingService.js';
 | 
			
		||||
import { UserKeypairService } from './UserKeypairService.js';
 | 
			
		||||
import { UserListService } from './UserListService.js';
 | 
			
		||||
@@ -173,6 +174,7 @@ const $SignupService: Provider = { provide: 'SignupService', useExisting: Signup
 | 
			
		||||
const $WebAuthnService: Provider = { provide: 'WebAuthnService', useExisting: WebAuthnService };
 | 
			
		||||
const $UserBlockingService: Provider = { provide: 'UserBlockingService', useExisting: UserBlockingService };
 | 
			
		||||
const $CacheService: Provider = { provide: 'CacheService', useExisting: CacheService };
 | 
			
		||||
const $UserService: Provider = { provide: 'UserService', useExisting: UserService };
 | 
			
		||||
const $UserFollowingService: Provider = { provide: 'UserFollowingService', useExisting: UserFollowingService };
 | 
			
		||||
const $UserKeypairService: Provider = { provide: 'UserKeypairService', useExisting: UserKeypairService };
 | 
			
		||||
const $UserListService: Provider = { provide: 'UserListService', useExisting: UserListService };
 | 
			
		||||
@@ -303,6 +305,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		WebAuthnService,
 | 
			
		||||
		UserBlockingService,
 | 
			
		||||
		CacheService,
 | 
			
		||||
		UserService,
 | 
			
		||||
		UserFollowingService,
 | 
			
		||||
		UserKeypairService,
 | 
			
		||||
		UserListService,
 | 
			
		||||
@@ -426,6 +429,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		$WebAuthnService,
 | 
			
		||||
		$UserBlockingService,
 | 
			
		||||
		$CacheService,
 | 
			
		||||
		$UserService,
 | 
			
		||||
		$UserFollowingService,
 | 
			
		||||
		$UserKeypairService,
 | 
			
		||||
		$UserListService,
 | 
			
		||||
@@ -550,6 +554,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		WebAuthnService,
 | 
			
		||||
		UserBlockingService,
 | 
			
		||||
		CacheService,
 | 
			
		||||
		UserService,
 | 
			
		||||
		UserFollowingService,
 | 
			
		||||
		UserKeypairService,
 | 
			
		||||
		UserListService,
 | 
			
		||||
@@ -672,6 +677,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
 | 
			
		||||
		$WebAuthnService,
 | 
			
		||||
		$UserBlockingService,
 | 
			
		||||
		$CacheService,
 | 
			
		||||
		$UserService,
 | 
			
		||||
		$UserFollowingService,
 | 
			
		||||
		$UserKeypairService,
 | 
			
		||||
		$UserListService,
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
 | 
			
		||||
import { setImmediate } from 'node:timers/promises';
 | 
			
		||||
import * as mfm from 'mfm-js';
 | 
			
		||||
import { In, DataSource, IsNull } from 'typeorm';
 | 
			
		||||
import { In, DataSource, IsNull, LessThan } from 'typeorm';
 | 
			
		||||
import * as Redis from 'ioredis';
 | 
			
		||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
 | 
			
		||||
import RE2 from 're2';
 | 
			
		||||
@@ -14,7 +14,7 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf
 | 
			
		||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
 | 
			
		||||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
 | 
			
		||||
import { MiNote } from '@/models/Note.js';
 | 
			
		||||
import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import type { MiDriveFile } from '@/models/DriveFile.js';
 | 
			
		||||
import type { MiApp } from '@/models/App.js';
 | 
			
		||||
import { concat } from '@/misc/prelude/array.js';
 | 
			
		||||
@@ -829,13 +829,12 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			// TODO: 休眠ユーザーを弾く
 | 
			
		||||
			// TODO: チャンネルフォロー
 | 
			
		||||
			// TODO: キャッシュ?
 | 
			
		||||
			const followings = await this.followingsRepository.find({
 | 
			
		||||
				where: {
 | 
			
		||||
					followeeId: user.id,
 | 
			
		||||
					followerHost: IsNull(),
 | 
			
		||||
					isFollowerHibernated: false,
 | 
			
		||||
				},
 | 
			
		||||
				select: ['followerId', 'withReplies'],
 | 
			
		||||
			});
 | 
			
		||||
@@ -952,11 +951,55 @@ export class NoteCreateService implements OnApplicationShutdown {
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (Math.random() < 0.1) {
 | 
			
		||||
				process.nextTick(() => {
 | 
			
		||||
					this.checkHibernation(followings);
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		redisPipeline.exec();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async checkHibernation(followings: MiFollowing[]) {
 | 
			
		||||
		if (followings.length === 0) return;
 | 
			
		||||
 | 
			
		||||
		const shuffle = (array: MiFollowing[]) => {
 | 
			
		||||
			for (let i = array.length - 1; i > 0; i--) {
 | 
			
		||||
				const j = Math.floor(Math.random() * (i + 1));
 | 
			
		||||
				[array[i], array[j]] = [array[j], array[i]];
 | 
			
		||||
			}
 | 
			
		||||
			return array;
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		// ランダムに最大1000件サンプリング
 | 
			
		||||
		const samples = shuffle(followings).slice(0, Math.min(followings.length, 1000));
 | 
			
		||||
 | 
			
		||||
		const hibernatedUsers = await this.usersRepository.find({
 | 
			
		||||
			where: {
 | 
			
		||||
				id: In(samples.map(x => x.followerId)),
 | 
			
		||||
				lastActiveDate: LessThan(new Date(Date.now() - (1000 * 60 * 60 * 24 * 50))),
 | 
			
		||||
			},
 | 
			
		||||
			select: ['id'],
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (hibernatedUsers.length > 0) {
 | 
			
		||||
			this.usersRepository.update({
 | 
			
		||||
				id: In(hibernatedUsers.map(x => x.id)),
 | 
			
		||||
			}, {
 | 
			
		||||
				isHibernated: true,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			this.followingsRepository.update({
 | 
			
		||||
				followerId: In(hibernatedUsers.map(x => x.id)),
 | 
			
		||||
			}, {
 | 
			
		||||
				isFollowerHibernated: true,
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public dispose(): void {
 | 
			
		||||
		this.#shutdownController.abort();
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										53
									
								
								packages/backend/src/core/UserService.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								packages/backend/src/core/UserService.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import type { FollowingsRepository, UsersRepository } from '@/models/_.js';
 | 
			
		||||
import type { MiUser } from '@/models/User.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class UserService {
 | 
			
		||||
	constructor(
 | 
			
		||||
		@Inject(DI.usersRepository)
 | 
			
		||||
		private usersRepository: UsersRepository,
 | 
			
		||||
 | 
			
		||||
		@Inject(DI.followingsRepository)
 | 
			
		||||
		private followingsRepository: FollowingsRepository,
 | 
			
		||||
	) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	public async updateLastActiveDate(user: MiUser): Promise<void> {
 | 
			
		||||
		if (user.isHibernated) {
 | 
			
		||||
			const result = await this.usersRepository.createQueryBuilder().update()
 | 
			
		||||
				.set({
 | 
			
		||||
					lastActiveDate: new Date(),
 | 
			
		||||
				})
 | 
			
		||||
				.where('id = :id', { id: user.id })
 | 
			
		||||
				.returning('*')
 | 
			
		||||
				.execute()
 | 
			
		||||
				.then((response) => {
 | 
			
		||||
					return response.raw[0];
 | 
			
		||||
				});
 | 
			
		||||
			const wokeUp = result.isHibernated;
 | 
			
		||||
			if (wokeUp) {
 | 
			
		||||
				this.usersRepository.update(user.id, {
 | 
			
		||||
					isHibernated: false,
 | 
			
		||||
				});
 | 
			
		||||
				this.followingsRepository.update({
 | 
			
		||||
					followerId: user.id,
 | 
			
		||||
				}, {
 | 
			
		||||
					isFollowerHibernated: false,
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			this.usersRepository.update(user.id, {
 | 
			
		||||
				lastActiveDate: new Date(),
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -9,7 +9,7 @@ import { MiUser } from './User.js';
 | 
			
		||||
 | 
			
		||||
@Entity('following')
 | 
			
		||||
@Index(['followerId', 'followeeId'], { unique: true })
 | 
			
		||||
@Index(['followeeId', 'followerHost'])
 | 
			
		||||
@Index(['followeeId', 'followerHost', 'isFollowerHibernated'])
 | 
			
		||||
export class MiFollowing {
 | 
			
		||||
	@PrimaryColumn(id())
 | 
			
		||||
	public id: string;
 | 
			
		||||
@@ -46,6 +46,11 @@ export class MiFollowing {
 | 
			
		||||
	@JoinColumn()
 | 
			
		||||
	public follower: MiUser | null;
 | 
			
		||||
 | 
			
		||||
	@Column('boolean', {
 | 
			
		||||
		default: false,
 | 
			
		||||
	})
 | 
			
		||||
	public isFollowerHibernated: boolean;
 | 
			
		||||
 | 
			
		||||
	// タイムラインにその人のリプライまで含めるかどうか
 | 
			
		||||
	@Column('boolean', {
 | 
			
		||||
		default: false,
 | 
			
		||||
 
 | 
			
		||||
@@ -187,6 +187,11 @@ export class MiUser {
 | 
			
		||||
	})
 | 
			
		||||
	public isExplorable: boolean;
 | 
			
		||||
 | 
			
		||||
	@Column('boolean', {
 | 
			
		||||
		default: false,
 | 
			
		||||
	})
 | 
			
		||||
	public isHibernated: boolean;
 | 
			
		||||
 | 
			
		||||
	// アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ
 | 
			
		||||
	@Column('boolean', {
 | 
			
		||||
		default: false,
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ import { NotificationService } from '@/core/NotificationService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { CacheService } from '@/core/CacheService.js';
 | 
			
		||||
import { MiLocalUser } from '@/models/User.js';
 | 
			
		||||
import { UserService } from '@/core/UserService.js';
 | 
			
		||||
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
 | 
			
		||||
import MainStreamConnection from './stream/Connection.js';
 | 
			
		||||
import { ChannelsService } from './stream/ChannelsService.js';
 | 
			
		||||
@@ -37,6 +38,7 @@ export class StreamingApiServerService {
 | 
			
		||||
		private authenticateService: AuthenticateService,
 | 
			
		||||
		private channelsService: ChannelsService,
 | 
			
		||||
		private notificationService: NotificationService,
 | 
			
		||||
		private usersService: UserService,
 | 
			
		||||
	) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -130,14 +132,10 @@ export class StreamingApiServerService {
 | 
			
		||||
			this.#connections.set(connection, Date.now());
 | 
			
		||||
 | 
			
		||||
			const userUpdateIntervalId = user ? setInterval(() => {
 | 
			
		||||
				this.usersRepository.update(user.id, {
 | 
			
		||||
					lastActiveDate: new Date(),
 | 
			
		||||
				});
 | 
			
		||||
				this.usersService.updateLastActiveDate(user);
 | 
			
		||||
			}, 1000 * 60 * 5) : null;
 | 
			
		||||
			if (user) {
 | 
			
		||||
				this.usersRepository.update(user.id, {
 | 
			
		||||
					lastActiveDate: new Date(),
 | 
			
		||||
				});
 | 
			
		||||
				this.usersService.updateLastActiveDate(user);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			connection.once('close', () => {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user