From 6a18812e9d823bc748169a332ce82d251a3cb267 Mon Sep 17 00:00:00 2001 From: Xeltica Date: Thu, 7 Jan 2021 01:29:52 +0900 Subject: [PATCH] resolve #12 --- migration/1609948116186-rating.ts | 18 ++++++++++ package.json | 2 ++ src/functions/format.ts | 4 +++ src/functions/ranking.ts | 14 ++++++++ src/functions/update-rating.ts | 13 +++++++ src/functions/update-score.ts | 15 ++++---- src/models/entities/user.ts | 18 ++++++++++ src/server/router.ts | 15 ++++++-- src/services/worker.ts | 18 ++++++---- src/tools/calculate-all-rating.ts | 7 ++++ src/tools/calculate-all-rating.worker.ts | 17 +++++++++ src/views/_components.pug | 46 +++++++++++++++--------- src/views/mypage.pug | 7 +++- src/views/ranking.pug | 8 +++++ src/views/welcome.pug | 2 +- yarn.lock | 5 +++ 16 files changed, 175 insertions(+), 34 deletions(-) create mode 100644 migration/1609948116186-rating.ts create mode 100644 src/functions/ranking.ts create mode 100644 src/functions/update-rating.ts create mode 100644 src/tools/calculate-all-rating.ts create mode 100644 src/tools/calculate-all-rating.worker.ts create mode 100644 src/views/ranking.pug diff --git a/migration/1609948116186-rating.ts b/migration/1609948116186-rating.ts new file mode 100644 index 0000000..2d03922 --- /dev/null +++ b/migration/1609948116186-rating.ts @@ -0,0 +1,18 @@ +import {MigrationInterface, QueryRunner} from 'typeorm'; + +export class rating1609948116186 implements MigrationInterface { + name = 'rating1609948116186' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "user" ADD "prevRating" real NOT NULL DEFAULT 0'); + await queryRunner.query('ALTER TABLE "user" ADD "rating" real NOT NULL DEFAULT 0'); + await queryRunner.query('ALTER TABLE "user" ADD "bannedFromRanking" boolean NOT NULL DEFAULT false'); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('ALTER TABLE "user" DROP COLUMN "bannedFromRanking"'); + await queryRunner.query('ALTER TABLE "user" DROP COLUMN "rating"'); + await queryRunner.query('ALTER TABLE "user" DROP COLUMN "prevRating"'); + } + +} diff --git a/package.json b/package.json index 72aba1a..59f82d9 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "build:styles": "sass styles/:built/assets", "build": "run-p build:*", "migrate": "ts-node --project ./tsconfig.migration.json ./node_modules/typeorm/cli.js migration:run", + "migrate:revert": "ts-node --project ./tsconfig.migration.json ./node_modules/typeorm/cli.js migration:revert", "dev": "nodemon" }, "dependencies": { @@ -26,6 +27,7 @@ "@types/node-cron": "^2.0.3", "@types/uuid": "^8.0.0", "axios": "^0.19.2", + "dayjs": "^1.10.2", "delay": "^4.4.0", "koa": "^2.13.0", "koa-bodyparser": "^4.3.0", diff --git a/src/functions/format.ts b/src/functions/format.ts index b306792..5ef3336 100644 --- a/src/functions/format.ts +++ b/src/functions/format.ts @@ -53,6 +53,10 @@ export const variables: Record = { description: '所属するインスタンスのホスト名', replace: (_, user) => String(user.host), }, + rating: { + description: 'みす廃レート', + replace: (_, user) => String(user.rating), + }, }; const variableRegex = /\{([a-zA-Z0-9_]+?)\}/g; diff --git a/src/functions/ranking.ts b/src/functions/ranking.ts new file mode 100644 index 0000000..cc3b105 --- /dev/null +++ b/src/functions/ranking.ts @@ -0,0 +1,14 @@ +import { Users } from '../models'; +import { User } from '../models/entities/user'; + +export const getRanking = async (limit: number | null = 10): Promise => { + const query = Users.createQueryBuilder('user') + .where('"user"."bannedFromRanking" IS NOT TRUE') + .orderBy('"user".rating', 'DESC'); + + if (limit !== null) { + query.limit(limit); + } + + return await query.getMany(); +}; \ No newline at end of file diff --git a/src/functions/update-rating.ts b/src/functions/update-rating.ts new file mode 100644 index 0000000..1760dd0 --- /dev/null +++ b/src/functions/update-rating.ts @@ -0,0 +1,13 @@ +import dayjs from 'dayjs'; + +import { User } from '../models/entities/user'; +import { updateUser } from './users'; +import { MiUser } from './update-score'; + +export const updateRating = async (user: User, miUser: MiUser): Promise => { + const elapsedDays = dayjs().diff(dayjs(miUser.createdAt), 'd') + 1; + await updateUser(user.username, user.host, { + prevRating: user.rating, + rating: miUser.notesCount / elapsedDays, + }); +}; diff --git a/src/functions/update-score.ts b/src/functions/update-score.ts index afdd32f..fabce6e 100644 --- a/src/functions/update-score.ts +++ b/src/functions/update-score.ts @@ -1,13 +1,14 @@ import { User } from '../models/entities/user'; -import { api } from '../services/misskey'; import { updateUser } from './users'; -export const updateScore = async (user: User): Promise => { - const miUser = await api>(user.host, 'users/show', { username: user.username }, user.token); - if (miUser.error) { - throw miUser.error; - } - +export type MiUser = { + notesCount: number, + followingCount: number, + followersCount: number, + createdAt: string, +}; + +export const updateScore = async (user: User, miUser: MiUser): Promise => { await updateUser(user.username, user.host, { prevNotesCount: miUser.notesCount ?? 0, prevFollowingCount: miUser.followingCount ?? 0, diff --git a/src/models/entities/user.ts b/src/models/entities/user.ts index 122d93f..92094d8 100644 --- a/src/models/entities/user.ts +++ b/src/models/entities/user.ts @@ -79,4 +79,22 @@ export class User { nullable: true, }) public template: string | null; + + @Column({ + type: 'real', + default: 0, + }) + public prevRating: number; + + @Column({ + type: 'real', + default: 0, + }) + public rating: number; + + @Column({ + type: 'boolean', + default: false, + }) + public bannedFromRanking: boolean; } \ No newline at end of file diff --git a/src/server/router.ts b/src/server/router.ts index 6875c17..b99ac41 100644 --- a/src/server/router.ts +++ b/src/server/router.ts @@ -14,6 +14,7 @@ import { Users } from '../models'; import { send } from '../services/send'; import { visibilities, Visibility } from '../types/Visibility'; import { defaultTemplate, variables } from '../functions/format'; +import { getRanking } from '../functions/ranking'; export const router = new Router(); @@ -61,23 +62,25 @@ router.get('/', async ctx => { const user = token ? await getUserByMisshaiToken(token) : undefined; const isAvailable = user && await apiAvailable(user.host, user.token); + const usersCount = await getUserCount(); + const ranking = await getRanking(10); + console.log(ranking); if (user && isAvailable) { const meta = await api<{ version: string }>(user?.host, 'meta', {}); await ctx.render('mypage', { - user, + user, usersCount, ranking, // To Activate Groundpolis Mode isGroundpolis: meta.version.includes('gp'), defaultTemplate, templateVariables: variables, - usersCount: await getUserCount(), score: await getScores(user), from: ctx.query.from, }); } else { // 非ログイン await ctx.render('welcome', { - usersCount: await getUserCount(), + usersCount, ranking, welcomeMessage: welcomeMessage[Math.floor(Math.random() * welcomeMessage.length)], from: ctx.query.from, }); @@ -144,6 +147,12 @@ router.get('/teapot', async ctx => { await die(ctx, 'I\'m a teapot', 418); }); +router.get('/ranking', async ctx => { + await ctx.render('ranking', { + ranking: await getRanking(null), + }); +}); + router.get('/miauth', async ctx => { const session = ctx.query.session as string | undefined; if (!session) { diff --git a/src/services/worker.ts b/src/services/worker.ts index b165759..5325a46 100644 --- a/src/services/worker.ts +++ b/src/services/worker.ts @@ -1,12 +1,14 @@ import cron from 'node-cron'; import delay from 'delay'; - -import { Users } from '../models'; -import { deleteUser } from '../functions/users'; -import { updateScore } from '../functions/update-score'; -import { send } from './send'; import { Not } from 'typeorm'; + +import { deleteUser } from '../functions/users'; +import { MiUser, updateScore } from '../functions/update-score'; +import { updateRating } from '../functions/update-rating'; import { AlertMode } from '../types/AlertMode'; +import { Users } from '../models'; +import { send } from './send'; +import { api } from './misskey'; export default (): void => { cron.schedule('0 0 0 * * *', async () => { @@ -14,7 +16,11 @@ export default (): void => { alertMode: Not('nothing'), }); for (const user of users) { + const miUser = await api(user.host, 'users/show', { username: user.username }, user.token); + try { + if (miUser.error) throw miUser.error; + await updateRating(user, miUser); await send(user); } catch (e) { if (e.code === 'NO_SUCH_USER' || e.code === 'AUTHENTICATION_FAILED') { @@ -27,7 +33,7 @@ export default (): void => { } finally { if (user.alertMode === 'note') await delay(3000); - await updateScore(user); + await updateScore(user, miUser); } } }); diff --git a/src/tools/calculate-all-rating.ts b/src/tools/calculate-all-rating.ts new file mode 100644 index 0000000..a59a62d --- /dev/null +++ b/src/tools/calculate-all-rating.ts @@ -0,0 +1,7 @@ +import { initDb } from '../services/db'; +import 'reflect-metadata'; + +(async () => { + await initDb(); + (await import('./calculate-all-rating.worker')).default(); +})(); \ No newline at end of file diff --git a/src/tools/calculate-all-rating.worker.ts b/src/tools/calculate-all-rating.worker.ts new file mode 100644 index 0000000..a8d9b01 --- /dev/null +++ b/src/tools/calculate-all-rating.worker.ts @@ -0,0 +1,17 @@ +import { Users } from '../models'; +import { updateRating } from '../functions/update-rating'; +import { api } from '../services/misskey'; +import { MiUser } from '../functions/update-score'; + +export default async () => { + const users = await Users.find(); + for (const u of users) { + console.log(`Update rating of ${u.username}@${u.host}...`); + const miUser = await api(u.host, 'users/show', { username: u.username }, u.token); + if (miUser.error) { + console.log(`Failed to fetch data of ${u.username}@${u.host}. Skipped`); + continue; + } + await updateRating(u, miUser); + } +}; \ No newline at end of file diff --git a/src/views/_components.pug b/src/views/_components.pug index a5d5d18..5010983 100644 --- a/src/views/_components.pug +++ b/src/views/_components.pug @@ -2,22 +2,36 @@ mixin exta() a(href=attributes.href target="_blank" rel="noopener noreferrer") block -mixin serverInfo() +mixin ranking() .xd-card .header - h1.title サービスの情報 + h1.title みす廃ランキング .body - dl - dt - i.fas.fa-users - | 登録者数 - dd !{usersCount} 人 - dt - | 総ノート数 - dd (coming soon) - dt - | 総フォロー数 - dd (coming soon) - dt - | 総フォロワー数 - dd (coming soon) \ No newline at end of file + p + i.fas.fa-users + strong 登録者数: !{usersCount}人 + +rankingTable() + p: a(href="/ranking") 全員分見る + +mixin rankingTable() + table + thead: tr + th 順位 + th ユーザー + th レート + tbody + - + let rank = 1; + let lastRating = ''; + console.log(ranking); + each rec in ranking + - const rating = rec.rating.toFixed(2); + tr + td=rank + td !{rec.username}@!{rec.host} + td=rating + - + if (lastRating !== rating) { + rank++; + } + lastRating = rating diff --git a/src/views/mypage.pug b/src/views/mypage.pug index 6967501..c48fed9 100644 --- a/src/views/mypage.pug +++ b/src/views/mypage.pug @@ -23,7 +23,7 @@ block content section#scores.xd-vstack .xd-hstack - +serverInfo() + +ranking() .xd-card .header h1.title みす廃データ @@ -47,6 +47,11 @@ block content td フォロワー td !{score.followersCount} td !{score.followersDelta} + tr + td フォロワー + td !{score.followersCount} + td !{score.followersDelta} + p みす廃レート: !{user.rating} section.xd-card#settings - diff --git a/src/views/ranking.pug b/src/views/ranking.pug new file mode 100644 index 0000000..884a848 --- /dev/null +++ b/src/views/ranking.pug @@ -0,0 +1,8 @@ +extends _base + +block content + .xd-card + h1: a(href="/") みす廃あらーと + section + h2 みす廃ランキング + +rankingTable \ No newline at end of file diff --git a/src/views/welcome.pug b/src/views/welcome.pug index c0b8870..1fa368a 100644 --- a/src/views/welcome.pug +++ b/src/views/welcome.pug @@ -23,7 +23,7 @@ block content input.xd-button.primary(type="submit", value="ログイン") section.xd-hstack - +serverInfo() + +ranking() .xd-card .header h1.title 開発者 diff --git a/yarn.lock b/yarn.lock index a0a137f..fe057f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -862,6 +862,11 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +dayjs@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.2.tgz#8f3a424ceb944a8193506804b0045a773d2d0672" + integrity sha512-h/YtykNNTR8Qgtd1Fxl5J1/SFP1b7SOk/M1P+Re+bCdFMV0IMkuKNgHPN7rlvvuhfw24w0LX78iYKt4YmePJNQ== + debug@=3.1.0, debug@~3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"