resolve #12
This commit is contained in:
parent
f08d1324d0
commit
6a18812e9d
18
migration/1609948116186-rating.ts
Normal file
18
migration/1609948116186-rating.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import {MigrationInterface, QueryRunner} from 'typeorm';
|
||||
|
||||
export class rating1609948116186 implements MigrationInterface {
|
||||
name = 'rating1609948116186'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
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"');
|
||||
}
|
||||
|
||||
}
|
@ -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",
|
||||
|
@ -53,6 +53,10 @@ export const variables: Record<string, Variable> = {
|
||||
description: '所属するインスタンスのホスト名',
|
||||
replace: (_, user) => String(user.host),
|
||||
},
|
||||
rating: {
|
||||
description: 'みす廃レート',
|
||||
replace: (_, user) => String(user.rating),
|
||||
},
|
||||
};
|
||||
|
||||
const variableRegex = /\{([a-zA-Z0-9_]+?)\}/g;
|
||||
|
14
src/functions/ranking.ts
Normal file
14
src/functions/ranking.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Users } from '../models';
|
||||
import { User } from '../models/entities/user';
|
||||
|
||||
export const getRanking = async (limit: number | null = 10): Promise<User[]> => {
|
||||
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();
|
||||
};
|
13
src/functions/update-rating.ts
Normal file
13
src/functions/update-rating.ts
Normal file
@ -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<void> => {
|
||||
const elapsedDays = dayjs().diff(dayjs(miUser.createdAt), 'd') + 1;
|
||||
await updateUser(user.username, user.host, {
|
||||
prevRating: user.rating,
|
||||
rating: miUser.notesCount / elapsedDays,
|
||||
});
|
||||
};
|
@ -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<void> => {
|
||||
const miUser = await api<Record<string, number>>(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<void> => {
|
||||
await updateUser(user.username, user.host, {
|
||||
prevNotesCount: miUser.notesCount ?? 0,
|
||||
prevFollowingCount: miUser.followingCount ?? 0,
|
||||
|
@ -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;
|
||||
}
|
@ -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<DefaultState, Context>();
|
||||
|
||||
@ -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) {
|
||||
|
@ -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<AlertMode>('nothing'),
|
||||
});
|
||||
for (const user of users) {
|
||||
const miUser = await api<MiUser & { error: unknown }>(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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
7
src/tools/calculate-all-rating.ts
Normal file
7
src/tools/calculate-all-rating.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { initDb } from '../services/db';
|
||||
import 'reflect-metadata';
|
||||
|
||||
(async () => {
|
||||
await initDb();
|
||||
(await import('./calculate-all-rating.worker')).default();
|
||||
})();
|
17
src/tools/calculate-all-rating.worker.ts
Normal file
17
src/tools/calculate-all-rating.worker.ts
Normal file
@ -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<MiUser & { error: unknown }>(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);
|
||||
}
|
||||
};
|
@ -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)
|
||||
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
|
||||
|
@ -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
|
||||
-
|
||||
|
8
src/views/ranking.pug
Normal file
8
src/views/ranking.pug
Normal file
@ -0,0 +1,8 @@
|
||||
extends _base
|
||||
|
||||
block content
|
||||
.xd-card
|
||||
h1: a(href="/") みす廃あらーと
|
||||
section
|
||||
h2 みす廃ランキング
|
||||
+rankingTable
|
@ -23,7 +23,7 @@ block content
|
||||
input.xd-button.primary(type="submit", value="ログイン")
|
||||
|
||||
section.xd-hstack
|
||||
+serverInfo()
|
||||
+ranking()
|
||||
.xd-card
|
||||
.header
|
||||
h1.title 開発者
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user