Skip to content
This repository was archived by the owner on Aug 29, 2020. It is now read-only.

Commit 969a245

Browse files
committed
init repo
0 parents  commit 969a245

20 files changed

+1103
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/**
2+
dist/**

.prettierrc

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"semi": true,
3+
"singleQuote": true
4+
}

.travis.yml

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
language: node_js
2+
node_js: 10
3+
4+
cache:
5+
yarn: true
6+
directories:
7+
- node_modules
8+
9+
install:
10+
- yarn
11+
12+
script:
13+
- yarn run build

.vscode/settings.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"editor.tabSize": 2
3+
}

.vscode/tasks.json

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
// 有关 tasks.json 格式的文档,请参见
3+
// https://go.microsoft.com/fwlink/?LinkId=733558
4+
"version": "2.0.0",
5+
"tasks": [
6+
{
7+
"type": "typescript",
8+
"tsconfig": "tsconfig.json",
9+
"problemMatcher": [
10+
"$tsc"
11+
],
12+
"group": {
13+
"kind": "build",
14+
"isDefault": true
15+
}
16+
}
17+
]
18+
}

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2019 XLor
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Codeforces Analytics
2+
3+
## Install
4+
5+
```bash
6+
yarn
7+
```
8+
9+
## Usage
10+
11+
### Start Daemon
12+
13+
```bash
14+
yarn start
15+
```
16+
17+
### command line
18+
19+
See cli help.
20+
21+
```bash
22+
yarn cli --help
23+
```

package.json

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "codeforces-analytics",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"author": "XLor",
6+
"license": "MIT",
7+
"scripts": {
8+
"start": "yarn tsc && node dist/index.js",
9+
"build": "yarn tsc",
10+
"server": "node dist/index.js",
11+
"cli": "node dist/cli/index.js",
12+
"lint": "yarn prettier --write src/**/*.ts"
13+
},
14+
"dependencies": {
15+
"axios": "^0.19.0",
16+
"body-parser": "^1.19.0",
17+
"express": "^4.17.1",
18+
"yargs": "^14.2.0"
19+
},
20+
"devDependencies": {
21+
"@types/express": "^4.17.1",
22+
"@types/node": "^12.11.1",
23+
"@types/yargs": "^13.0.3",
24+
"cross-env": "^6.0.3",
25+
"prettier": "^1.18.2",
26+
"typescript": "^3.6.4"
27+
}
28+
}

src/analytics/api.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import axios from 'axios';
2+
3+
const api = axios.create({
4+
baseURL: 'https://codeforces.com/api/'
5+
});
6+
7+
export default api;
8+
9+
const userInfoCache = new Map<string, object>();
10+
11+
export async function getUserInfo(cfid: string) {
12+
if (userInfoCache.has(cfid)) {
13+
return userInfoCache.get(cfid);
14+
}
15+
const {
16+
data: {
17+
result: [data]
18+
}
19+
} = await api.get(`user.info`, {
20+
params: {
21+
handles: cfid
22+
}
23+
});
24+
userInfoCache.set(cfid, data);
25+
return data;
26+
}

src/analytics/index.ts

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Router } from 'express';
2+
3+
import { getUserInfo } from './api';
4+
import { User } from '../type';
5+
import { insert, query } from './store';
6+
7+
const router = Router();
8+
9+
router.get('/', (req, res) => {
10+
if ('name' in req.query) {
11+
try {
12+
res.send(query(req.query.name));
13+
} catch (err) {
14+
res.status(404).send();
15+
}
16+
} else {
17+
res.send(query());
18+
}
19+
});
20+
21+
router.post('/', async (req, res) => {
22+
const { name, cfid } = req.body;
23+
try {
24+
const info = await getUserInfo(cfid);
25+
const handle = new User(info);
26+
insert(name, handle);
27+
res.send(info);
28+
} catch (err) {
29+
res.status(500).send('');
30+
}
31+
});
32+
33+
export default router;

src/analytics/store.ts

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { User } from '../type';
2+
import assert from 'assert';
3+
4+
const store = new Map<string, User>();
5+
6+
export function insert(name: string, handle: User) {
7+
if (store.has(name)) {
8+
const old: User = store.get(name) as User;
9+
if (old.maxRating > handle.maxRating) {
10+
handle.maxRating = old.maxRating;
11+
handle.maxRank = old.maxRank;
12+
}
13+
if (old.rating > handle.rating) {
14+
handle.rating = old.rating;
15+
handle.rank = old.rank;
16+
}
17+
handle.cfids = [...new Set([...old.cfids, ...handle.cfids])];
18+
store.set(name, handle);
19+
} else {
20+
store.set(name, handle);
21+
}
22+
}
23+
24+
export function query(): Array<User & { name: string }>;
25+
export function query(name: string): User;
26+
27+
export function query(name?: string): any {
28+
if (typeof name === 'undefined') {
29+
const arr: Array<any> = [];
30+
store.forEach((value: User, key: string) => {
31+
arr.push({ name: key, ...value });
32+
});
33+
return arr;
34+
} else if (typeof name === 'string') {
35+
assert(store.has(name));
36+
return store.get(name);
37+
}
38+
}

src/app.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import express from 'express';
2+
import bodyParser from 'body-parser';
3+
4+
import { DEFAULT_HOST, DEFAULT_PORT } from './config';
5+
6+
import anyRouter from './analytics';
7+
8+
const app = express();
9+
10+
app.set('host', process.env.HOST ? process.env.HOST : DEFAULT_HOST);
11+
app.set('port', process.env.PORT ? process.env.PORT : DEFAULT_PORT);
12+
13+
app.use(bodyParser.json());
14+
15+
app.use('/', anyRouter);
16+
17+
export default app;

src/cli/api.ts

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import axios from 'axios';
2+
3+
import { DAEMON_URL } from '../config';
4+
import { User } from '../type';
5+
6+
const api = axios.create({
7+
baseURL: DAEMON_URL,
8+
timeout: 60 * 1000
9+
});
10+
11+
export default api;
12+
13+
export async function checkDaemon() {
14+
try {
15+
await api.get('');
16+
return true;
17+
} catch (err) {
18+
return false;
19+
}
20+
}
21+
22+
export async function insert(name: string, cfid: string) {
23+
console.log(`开始查询 "${cfid}" ...`);
24+
try {
25+
await api.post('', { name, cfid });
26+
console.log(`${name}的 Codeforces ID "${cfid}" 插入成功`);
27+
return true;
28+
} catch (err) {
29+
console.log(`插入失败`);
30+
return false;
31+
}
32+
}
33+
34+
export async function query(): Promise<Array<User & { name: string }>>;
35+
export async function query(name: string): Promise<User>;
36+
37+
export async function query(name?: string): Promise<any> {
38+
if (typeof name === 'undefined') {
39+
const { data } = await api.get('');
40+
return data;
41+
} else if (typeof name === 'string') {
42+
const { data } = await api.get('', {
43+
params: { name }
44+
});
45+
return data;
46+
}
47+
}

src/cli/index.ts

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { writeFile } from 'fs';
2+
3+
import yargs from 'yargs';
4+
import { checkDaemon, insert, query } from './api';
5+
6+
(async function() {
7+
if (!(await checkDaemon())) {
8+
console.log('未检测到守护进程的运行');
9+
process.exit(1);
10+
} else {
11+
yargs
12+
.command(
13+
['add <name> <cfid>', '$0'],
14+
'添加一条 Codeforces ID',
15+
yargs => {
16+
return yargs.options({
17+
name: {
18+
type: 'string',
19+
describe: '你的姓名',
20+
demandOption: true
21+
},
22+
cfid: {
23+
type: 'string',
24+
describe: '你的 Codeforces ID',
25+
demandOption: true
26+
}
27+
});
28+
},
29+
argv => {
30+
insert(argv.name, argv.cfid);
31+
}
32+
)
33+
.command(
34+
'query [file]',
35+
'查询所有用户信息 / 姓名为 name 的用户信息',
36+
yargs => {
37+
return yargs.options({
38+
name: {
39+
type: 'string',
40+
describe: '查询对象姓名'
41+
},
42+
file: {
43+
type: 'string',
44+
describe: '输出文件名'
45+
}
46+
});
47+
},
48+
async argv => {
49+
const res =
50+
typeof argv.name === 'undefined'
51+
? await query()
52+
: await query(argv.name as string);
53+
if (typeof argv.file === 'undefined') {
54+
console.log(res);
55+
} else {
56+
writeFile(argv.file, JSON.stringify(res), () => {});
57+
}
58+
}
59+
)
60+
.help().argv;
61+
}
62+
})();

src/config.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const DEFAULT_HOST = 'localhost';
2+
export const DEFAULT_PORT = 2333;
3+
export const DAEMON_URL = `http://${DEFAULT_HOST}:${DEFAULT_PORT}`;

src/index.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import app from './app';
2+
3+
const server = app.listen(app.get('port'), app.get('host'), () => {
4+
console.log(
5+
`\n Server starts on http://${app.get('host')}:${app.get('port')}\n`
6+
);
7+
});
8+
9+
export default server;

0 commit comments

Comments
 (0)