This commit is contained in:
Xeltica 2021-01-07 01:29:52 +09:00
parent f08d1324d0
commit 6a18812e9d
16 changed files with 175 additions and 34 deletions

View 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"');
}
}

View File

@ -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",

View File

@ -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
View 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();
};

View 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,
});
};

View File

@ -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,

View File

@ -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;
}

View File

@ -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) {

View File

@ -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);
}
}
});

View File

@ -0,0 +1,7 @@
import { initDb } from '../services/db';
import 'reflect-metadata';
(async () => {
await initDb();
(await import('./calculate-all-rating.worker')).default();
})();

View 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);
}
};

View File

@ -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

View File

@ -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
View File

@ -0,0 +1,8 @@
extends _base
block content
.xd-card
h1: a(href="/") みす廃あらーと
section
h2 みす廃ランキング
+rankingTable

View File

@ -23,7 +23,7 @@ block content
input.xd-button.primary(type="submit", value="ログイン")
section.xd-hstack
+serverInfo()
+ranking()
.xd-card
.header
h1.title 開発者

View File

@ -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"