diff --git a/packages/backend/src/core/chart/charts/per-user-pv.ts b/packages/backend/src/core/chart/charts/per-user-pv.ts index 31708fefa8..40ff9061f6 100644 --- a/packages/backend/src/core/chart/charts/per-user-pv.ts +++ b/packages/backend/src/core/chart/charts/per-user-pv.ts @@ -52,4 +52,12 @@ export default class PerUserPvChart extends Chart { // eslint-dis 'pv.visitor': 1, }, user.id); } + + @bindThis + public async getChartUsers(span: 'hour' | 'day', amount: number, cursor: Date | null, limit = 0, offset = 0): Promise<{ + userId: string; + count: number; +}[]> { + return await this.getChartPv(span, amount, cursor, limit, offset); + } } diff --git a/packages/backend/src/core/chart/core.ts b/packages/backend/src/core/chart/core.ts index af5485a46e..f0d4fb25c2 100644 --- a/packages/backend/src/core/chart/core.ts +++ b/packages/backend/src/core/chart/core.ts @@ -15,7 +15,7 @@ import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/misc import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { MiRepository, miRepository } from '@/models/_.js'; -import type { DataSource, Repository } from 'typeorm'; +import type { DataSource, ObjectLiteral, Repository } from 'typeorm'; const COLUMN_PREFIX = '___' as const; const UNIQUE_TEMP_COLUMN_PREFIX = 'unique_temp___' as const; @@ -94,6 +94,8 @@ type ToJsonSchema = { required: (keyof S)[]; }; +type MiAndOrmRepository = Repository & MiRepository; + export function getJsonSchema(schema: S): ToJsonSchema>> { const unflatten = (str: string, parent: Record) => { const keys = str.split('.'); @@ -146,11 +148,12 @@ export default abstract class Chart { group: string | null; }[] = []; // ↓にしたいけどfindOneとかで型エラーになる - //private repositoryForHour: Repository> & MiRepository>; - //private repositoryForDay: Repository> & MiRepository>; - private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>; - private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>; - + //private repositoryForHour: MiAndOrmRepository>; + //private repositoryForDay: MiAndOrmRepository>; + private repositoryForHour: MiAndOrmRepository<{ id: number; group?: string | null; date: number;}>; + private repositoryForDay: MiAndOrmRepository<{ id: number; group?: string | null; date: number;}>; + private repositoryUserPvForHour: MiAndOrmRepository<{ id: number; group?: string | null; date: number; ___pv_user:number; ___upv_user:number; ___pv_visitor:number; ___upv_visitor:number;}>; + private repositoryUserPvForDay: MiAndOrmRepository<{ id: number; group?: string | null; date: number; ___pv_user:number; ___upv_user:number; ___pv_visitor:number; ___upv_visitor:number;}>; /** * 1日に一回程度実行されれば良いような計算処理を入れる(主にCASCADE削除などアプリケーション側で感知できない変動によるズレの修正用) */ @@ -276,8 +279,10 @@ export default abstract class Chart { this.logger = logger; const { hour, day } = Chart.schemaToEntity(name, schema, grouped); - this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>); - this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>); + this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour).extend(miRepository); + this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day).extend(miRepository); + this.repositoryUserPvForHour = db.getRepository<{ id: number; group?: string | null; date: number; ___pv_user:number; ___upv_user:number; ___pv_visitor:number; ___upv_visitor:number;}>(hour).extend(miRepository); + this.repositoryUserPvForDay = db.getRepository<{ id: number; group?: string | null; date: number; ___pv_user:number; ___upv_user:number; ___pv_visitor:number; ___upv_visitor:number;}>(day).extend(miRepository); } @bindThis @@ -727,4 +732,51 @@ export default abstract class Chart { } return object as Unflatten>; } + + @bindThis + public async getChartPv(span: 'hour' | 'day', amount: number, cursor: Date | null, limit: number, offset: number): Promise< + { + userId: string, + count: number, + }[] + > { + const [y, m, d, h, _m, _s, _ms] = cursor ? Chart.parseDate(subtractTime(addTime(cursor, 1, span), 1)) : Chart.getCurrentDate(); + const [y2, m2, d2, h2] = cursor ? Chart.parseDate(addTime(cursor, 1, span)) : [] as never; + + const lt = dateUTC([y, m, d, h, _m, _s, _ms]); + + const gt = + span === 'day' ? subtractTime(cursor ? dateUTC([y2, m2, d2, 0]) : dateUTC([y, m, d, 0]), amount - 1, 'day') : + span === 'hour' ? subtractTime(cursor ? dateUTC([y2, m2, d2, h2]) : dateUTC([y, m, d, h]), amount - 1, 'hour') : + new Error('not happen') as never; + + const repository = + span === 'hour' ? this.repositoryUserPvForHour : + span === 'day' ? this.repositoryUserPvForDay : + new Error('not happen') as never; + + // ログ取得 + const logs = await repository.createQueryBuilder() + .where('date BETWEEN :gt AND :lt', { gt: Chart.dateToTimestamp(gt), lt: Chart.dateToTimestamp(lt) }) + .orderBy('___pv_visitor + ___upv_visitor + ___pv_user + ___upv_user', 'DESC') + .skip(offset) + .take(limit) + .getMany() as { + ___pv_visitor: number, + ___upv_visitor: number, + ___pv_user: number, + ___upv_user: number, + group: string, + }[]; + const result = [] as { + userId: string, + count: number, + }[]; + for (const row of logs) { + const userId = row.group; + const count = row.___pv_user + row.___upv_user + row.___pv_visitor + row.___upv_visitor; + result.push({ userId, count }); + } + return result; + } } diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts index e845853017..a424d7a4c7 100644 --- a/packages/backend/src/server/api/endpoints/users.ts +++ b/packages/backend/src/server/api/endpoints/users.ts @@ -6,6 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import type { UsersRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; +import PerUserPvChart from '@/core/chart/charts/per-user-pv.js'; import { QueryService } from '@/core/QueryService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DI } from '@/di-symbols.js'; @@ -31,7 +32,7 @@ export const paramDef = { properties: { limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, offset: { type: 'integer', default: 0 }, - sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] }, + sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt', '+pv', '-pv'] }, state: { type: 'string', enum: ['all', 'alive'], default: 'all' }, origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' }, hostname: { @@ -49,6 +50,7 @@ export default class extends Endpoint { // eslint- constructor( @Inject(DI.usersRepository) private usersRepository: UsersRepository, + private perUserPvChart: PerUserPvChart, private userEntityService: UserEntityService, private queryService: QueryService, @@ -70,7 +72,12 @@ export default class extends Endpoint { // eslint- if (ps.hostname) { query.andWhere('user.host = :hostname', { hostname: ps.hostname.toLowerCase() }); } - + const chartUsers: { userId: string; count: number; }[] = []; + if (ps.sort?.endsWith('pv')) { + await this.perUserPvChart.getChartUsers('hour', 0, null, ps.limit, ps.offset).then(users => { + chartUsers.push(...users); + }); + } switch (ps.sort) { case '+follower': query.orderBy('user.followersCount', 'DESC'); break; case '-follower': query.orderBy('user.followersCount', 'ASC'); break; @@ -78,6 +85,16 @@ export default class extends Endpoint { // eslint- case '-createdAt': query.orderBy('user.id', 'ASC'); break; case '+updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'DESC'); break; case '-updatedAt': query.andWhere('user.updatedAt IS NOT NULL').orderBy('user.updatedAt', 'ASC'); break; + case '+pv': + if (chartUsers.length > 0) { + query.andWhere('user.id IN (:...userIds)', { userIds: chartUsers.map(user => user.userId) }); + } + break; + case '-pv': + if (chartUsers.length > 0) { + query.andWhere('user.id IN (:...userIds)', { userIds: chartUsers.map(user => user.userId) }); + } + break; default: query.orderBy('user.id', 'ASC'); break; } @@ -88,6 +105,19 @@ export default class extends Endpoint { // eslint- query.offset(ps.offset); const users = await query.getMany(); + if (ps.sort === '+pv') { + users.sort((a, b) => { + const aPv = chartUsers.find(user => user.userId === a.id)?.count ?? 0; + const bPv = chartUsers.find(user => user.userId === b.id)?.count ?? 0; + return bPv - aPv; + }); + } else if (ps.sort === '-pv') { + users.sort((a, b) => { + const aPv = chartUsers.find(user => user.userId === a.id)?.count ?? 0; + const bPv = chartUsers.find(user => user.userId === b.id)?.count ?? 0; + return aPv - bPv; + }); + } return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' }); }); diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index ff731a2fa6..25874d63c5 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -24986,7 +24986,7 @@ export type operations = { /** @default 0 */ offset?: number; /** @enum {string} */ - sort?: '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+updatedAt' | '-updatedAt'; + sort?: '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+updatedAt' | '-updatedAt' | '+pv' | '-pv'; /** * @default all * @enum {string}