resolve #12
This commit is contained in:
parent
f08d1324d0
commit
6a18812e9d
16 changed files with 175 additions and 34 deletions
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:styles": "sass styles/:built/assets",
|
||||||
"build": "run-p build:*",
|
"build": "run-p build:*",
|
||||||
"migrate": "ts-node --project ./tsconfig.migration.json ./node_modules/typeorm/cli.js migration:run",
|
"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"
|
"dev": "nodemon"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -26,6 +27,7 @@
|
||||||
"@types/node-cron": "^2.0.3",
|
"@types/node-cron": "^2.0.3",
|
||||||
"@types/uuid": "^8.0.0",
|
"@types/uuid": "^8.0.0",
|
||||||
"axios": "^0.19.2",
|
"axios": "^0.19.2",
|
||||||
|
"dayjs": "^1.10.2",
|
||||||
"delay": "^4.4.0",
|
"delay": "^4.4.0",
|
||||||
"koa": "^2.13.0",
|
"koa": "^2.13.0",
|
||||||
"koa-bodyparser": "^4.3.0",
|
"koa-bodyparser": "^4.3.0",
|
||||||
|
|
|
@ -53,6 +53,10 @@ export const variables: Record<string, Variable> = {
|
||||||
description: '所属するインスタンスのホスト名',
|
description: '所属するインスタンスのホスト名',
|
||||||
replace: (_, user) => String(user.host),
|
replace: (_, user) => String(user.host),
|
||||||
},
|
},
|
||||||
|
rating: {
|
||||||
|
description: 'みす廃レート',
|
||||||
|
replace: (_, user) => String(user.rating),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const variableRegex = /\{([a-zA-Z0-9_]+?)\}/g;
|
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 { User } from '../models/entities/user';
|
||||||
import { api } from '../services/misskey';
|
|
||||||
import { updateUser } from './users';
|
import { updateUser } from './users';
|
||||||
|
|
||||||
export const updateScore = async (user: User): Promise<void> => {
|
export type MiUser = {
|
||||||
const miUser = await api<Record<string, number>>(user.host, 'users/show', { username: user.username }, user.token);
|
notesCount: number,
|
||||||
if (miUser.error) {
|
followingCount: number,
|
||||||
throw miUser.error;
|
followersCount: number,
|
||||||
}
|
createdAt: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateScore = async (user: User, miUser: MiUser): Promise<void> => {
|
||||||
await updateUser(user.username, user.host, {
|
await updateUser(user.username, user.host, {
|
||||||
prevNotesCount: miUser.notesCount ?? 0,
|
prevNotesCount: miUser.notesCount ?? 0,
|
||||||
prevFollowingCount: miUser.followingCount ?? 0,
|
prevFollowingCount: miUser.followingCount ?? 0,
|
||||||
|
|
|
@ -79,4 +79,22 @@ export class User {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
public template: string | null;
|
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 { send } from '../services/send';
|
||||||
import { visibilities, Visibility } from '../types/Visibility';
|
import { visibilities, Visibility } from '../types/Visibility';
|
||||||
import { defaultTemplate, variables } from '../functions/format';
|
import { defaultTemplate, variables } from '../functions/format';
|
||||||
|
import { getRanking } from '../functions/ranking';
|
||||||
|
|
||||||
export const router = new Router<DefaultState, Context>();
|
export const router = new Router<DefaultState, Context>();
|
||||||
|
|
||||||
|
@ -61,23 +62,25 @@ router.get('/', async ctx => {
|
||||||
const user = token ? await getUserByMisshaiToken(token) : undefined;
|
const user = token ? await getUserByMisshaiToken(token) : undefined;
|
||||||
|
|
||||||
const isAvailable = user && await apiAvailable(user.host, user.token);
|
const isAvailable = user && await apiAvailable(user.host, user.token);
|
||||||
|
const usersCount = await getUserCount();
|
||||||
|
const ranking = await getRanking(10);
|
||||||
|
console.log(ranking);
|
||||||
|
|
||||||
if (user && isAvailable) {
|
if (user && isAvailable) {
|
||||||
const meta = await api<{ version: string }>(user?.host, 'meta', {});
|
const meta = await api<{ version: string }>(user?.host, 'meta', {});
|
||||||
await ctx.render('mypage', {
|
await ctx.render('mypage', {
|
||||||
user,
|
user, usersCount, ranking,
|
||||||
// To Activate Groundpolis Mode
|
// To Activate Groundpolis Mode
|
||||||
isGroundpolis: meta.version.includes('gp'),
|
isGroundpolis: meta.version.includes('gp'),
|
||||||
defaultTemplate,
|
defaultTemplate,
|
||||||
templateVariables: variables,
|
templateVariables: variables,
|
||||||
usersCount: await getUserCount(),
|
|
||||||
score: await getScores(user),
|
score: await getScores(user),
|
||||||
from: ctx.query.from,
|
from: ctx.query.from,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 非ログイン
|
// 非ログイン
|
||||||
await ctx.render('welcome', {
|
await ctx.render('welcome', {
|
||||||
usersCount: await getUserCount(),
|
usersCount, ranking,
|
||||||
welcomeMessage: welcomeMessage[Math.floor(Math.random() * welcomeMessage.length)],
|
welcomeMessage: welcomeMessage[Math.floor(Math.random() * welcomeMessage.length)],
|
||||||
from: ctx.query.from,
|
from: ctx.query.from,
|
||||||
});
|
});
|
||||||
|
@ -144,6 +147,12 @@ router.get('/teapot', async ctx => {
|
||||||
await die(ctx, 'I\'m a teapot', 418);
|
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 => {
|
router.get('/miauth', async ctx => {
|
||||||
const session = ctx.query.session as string | undefined;
|
const session = ctx.query.session as string | undefined;
|
||||||
if (!session) {
|
if (!session) {
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import cron from 'node-cron';
|
import cron from 'node-cron';
|
||||||
import delay from 'delay';
|
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 { 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 { AlertMode } from '../types/AlertMode';
|
||||||
|
import { Users } from '../models';
|
||||||
|
import { send } from './send';
|
||||||
|
import { api } from './misskey';
|
||||||
|
|
||||||
export default (): void => {
|
export default (): void => {
|
||||||
cron.schedule('0 0 0 * * *', async () => {
|
cron.schedule('0 0 0 * * *', async () => {
|
||||||
|
@ -14,7 +16,11 @@ export default (): void => {
|
||||||
alertMode: Not<AlertMode>('nothing'),
|
alertMode: Not<AlertMode>('nothing'),
|
||||||
});
|
});
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
|
const miUser = await api<MiUser & { error: unknown }>(user.host, 'users/show', { username: user.username }, user.token);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (miUser.error) throw miUser.error;
|
||||||
|
await updateRating(user, miUser);
|
||||||
await send(user);
|
await send(user);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.code === 'NO_SUCH_USER' || e.code === 'AUTHENTICATION_FAILED') {
|
if (e.code === 'NO_SUCH_USER' || e.code === 'AUTHENTICATION_FAILED') {
|
||||||
|
@ -27,7 +33,7 @@ export default (): void => {
|
||||||
} finally {
|
} finally {
|
||||||
if (user.alertMode === 'note')
|
if (user.alertMode === 'note')
|
||||||
await delay(3000);
|
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")
|
a(href=attributes.href target="_blank" rel="noopener noreferrer")
|
||||||
block
|
block
|
||||||
|
|
||||||
mixin serverInfo()
|
mixin ranking()
|
||||||
.xd-card
|
.xd-card
|
||||||
.header
|
.header
|
||||||
h1.title サービスの情報
|
h1.title みす廃ランキング
|
||||||
.body
|
.body
|
||||||
dl
|
p
|
||||||
dt
|
|
||||||
i.fas.fa-users
|
i.fas.fa-users
|
||||||
| 登録者数
|
strong 登録者数: !{usersCount}人
|
||||||
dd !{usersCount} 人
|
+rankingTable()
|
||||||
dt
|
p: a(href="/ranking") 全員分見る
|
||||||
| 総ノート数
|
|
||||||
dd (coming soon)
|
mixin rankingTable()
|
||||||
dt
|
table
|
||||||
| 総フォロー数
|
thead: tr
|
||||||
dd (coming soon)
|
th 順位
|
||||||
dt
|
th ユーザー
|
||||||
| 総フォロワー数
|
th レート
|
||||||
dd (coming soon)
|
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
|
section#scores.xd-vstack
|
||||||
.xd-hstack
|
.xd-hstack
|
||||||
+serverInfo()
|
+ranking()
|
||||||
.xd-card
|
.xd-card
|
||||||
.header
|
.header
|
||||||
h1.title みす廃データ
|
h1.title みす廃データ
|
||||||
|
@ -47,6 +47,11 @@ block content
|
||||||
td フォロワー
|
td フォロワー
|
||||||
td !{score.followersCount}
|
td !{score.followersCount}
|
||||||
td !{score.followersDelta}
|
td !{score.followersDelta}
|
||||||
|
tr
|
||||||
|
td フォロワー
|
||||||
|
td !{score.followersCount}
|
||||||
|
td !{score.followersDelta}
|
||||||
|
p みす廃レート: !{user.rating}
|
||||||
|
|
||||||
section.xd-card#settings
|
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="ログイン")
|
input.xd-button.primary(type="submit", value="ログイン")
|
||||||
|
|
||||||
section.xd-hstack
|
section.xd-hstack
|
||||||
+serverInfo()
|
+ranking()
|
||||||
.xd-card
|
.xd-card
|
||||||
.header
|
.header
|
||||||
h1.title 開発者
|
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"
|
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
|
||||||
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
|
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:
|
debug@=3.1.0, debug@~3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue