ES Modulesに移行
This commit is contained in:
parent
0c3df4245d
commit
69212dd99a
105 changed files with 3154 additions and 3230 deletions
|
@ -11,40 +11,40 @@ export const defaultTemplate = '昨日のMisskeyの活動は\n\nノート: {note
|
|||
export const currentTokenVersion = 2;
|
||||
|
||||
export const misskeyAppInfo = {
|
||||
name: 'Misskey Tools',
|
||||
description: 'A Professional Toolkit Designed for Misskey.',
|
||||
permission: [
|
||||
'read:account',
|
||||
'write:account',
|
||||
'read:blocks',
|
||||
'write:blocks',
|
||||
'read:drive',
|
||||
'write:drive',
|
||||
'read:favorites',
|
||||
'write:favorites',
|
||||
'read:following',
|
||||
'write:following',
|
||||
'read:messaging',
|
||||
'write:messaging',
|
||||
'read:mutes',
|
||||
'write:mutes',
|
||||
'write:notes',
|
||||
'read:notifications',
|
||||
'write:notifications',
|
||||
'read:reactions',
|
||||
'write:reactions',
|
||||
'write:votes',
|
||||
'read:pages',
|
||||
'write:pages',
|
||||
'write:page-likes',
|
||||
'read:page-likes',
|
||||
'read:user-groups',
|
||||
'write:user-groups',
|
||||
'read:channels',
|
||||
'write:channels',
|
||||
'read:gallery',
|
||||
'write:gallery',
|
||||
'read:gallery-likes',
|
||||
'write:gallery-likes',
|
||||
],
|
||||
name: 'Misskey Tools',
|
||||
description: 'A Professional Toolkit Designed for Misskey.',
|
||||
permission: [
|
||||
'read:account',
|
||||
'write:account',
|
||||
'read:blocks',
|
||||
'write:blocks',
|
||||
'read:drive',
|
||||
'write:drive',
|
||||
'read:favorites',
|
||||
'write:favorites',
|
||||
'read:following',
|
||||
'write:following',
|
||||
'read:messaging',
|
||||
'write:messaging',
|
||||
'read:mutes',
|
||||
'write:mutes',
|
||||
'write:notes',
|
||||
'read:notifications',
|
||||
'write:notifications',
|
||||
'read:reactions',
|
||||
'write:reactions',
|
||||
'write:votes',
|
||||
'read:pages',
|
||||
'write:pages',
|
||||
'write:page-likes',
|
||||
'read:page-likes',
|
||||
'read:user-groups',
|
||||
'write:user-groups',
|
||||
'read:channels',
|
||||
'write:channels',
|
||||
'read:gallery',
|
||||
'write:gallery',
|
||||
'read:gallery-likes',
|
||||
'write:gallery-likes',
|
||||
],
|
||||
} as const;
|
||||
|
|
|
@ -4,38 +4,38 @@
|
|||
*/
|
||||
|
||||
import { BadRequestError, CurrentUser, Get, JsonController, OnUndefined, Post } from 'routing-controllers';
|
||||
import { IUser } from '../../common/types/user';
|
||||
import { config } from '../../config';
|
||||
import { work } from '../services/worker';
|
||||
import * as Store from '../store';
|
||||
import { IUser } from '../../common/types/user.js';
|
||||
import { config } from '../../config.js';
|
||||
import { work } from '../services/worker.js';
|
||||
import * as Store from '../store.js';
|
||||
|
||||
@JsonController('/admin')
|
||||
@JsonController('/admin')
|
||||
export class AdminController {
|
||||
@Get() getAdmin() {
|
||||
const { username, host } = config.admin;
|
||||
return {
|
||||
username, host,
|
||||
acct: `@${username}@${host}`,
|
||||
};
|
||||
}
|
||||
@Get() getAdmin() {
|
||||
const { username, host } = config.admin;
|
||||
return {
|
||||
username, host,
|
||||
acct: `@${username}@${host}`,
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/misshai/log') getMisshaiLog(@CurrentUser({ required: true }) user: IUser) {
|
||||
if (!user.isAdmin) {
|
||||
throw new BadRequestError('Not an Admin');
|
||||
}
|
||||
@Get('/misshai/log') getMisshaiLog(@CurrentUser({ required: true }) user: IUser) {
|
||||
if (!user.isAdmin) {
|
||||
throw new BadRequestError('Not an Admin');
|
||||
}
|
||||
|
||||
return Store.getState().misshaiWorkerLog;
|
||||
}
|
||||
return Store.getState().misshaiWorkerLog;
|
||||
}
|
||||
|
||||
@OnUndefined(204)
|
||||
@Post('/misshai/start') startMisshai(@CurrentUser({ required: true }) user: IUser) {
|
||||
if (!user.isAdmin) {
|
||||
throw new BadRequestError('Not an Admin');
|
||||
}
|
||||
if (Store.getState().nowCalculating) {
|
||||
throw new BadRequestError('Already started');
|
||||
}
|
||||
@OnUndefined(204)
|
||||
@Post('/misshai/start') startMisshai(@CurrentUser({ required: true }) user: IUser) {
|
||||
if (!user.isAdmin) {
|
||||
throw new BadRequestError('Not an Admin');
|
||||
}
|
||||
if (Store.getState().nowCalculating) {
|
||||
throw new BadRequestError('Already started');
|
||||
}
|
||||
|
||||
work();
|
||||
}
|
||||
work();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,101 +4,101 @@
|
|||
*/
|
||||
|
||||
import { BadRequestError, Body, CurrentUser, Delete, Get, JsonController, NotFoundError, OnUndefined, Param, Post, Put } from 'routing-controllers';
|
||||
import { IUser } from '../../common/types/user';
|
||||
import { Announcements } from '../models';
|
||||
import { AnnounceCreate } from './body/announce-create';
|
||||
import { AnnounceUpdate } from './body/announce-update';
|
||||
import { IdProp } from './body/id-prop';
|
||||
import { IUser } from '../../common/types/user.js';
|
||||
import { Announcements } from '../models/index.js';
|
||||
import { AnnounceCreate } from './body/announce-create.js';
|
||||
import { AnnounceUpdate } from './body/announce-update.js';
|
||||
import { IdProp } from './body/id-prop.js';
|
||||
|
||||
@JsonController('/announcements')
|
||||
export class AdminController {
|
||||
@Get() get() {
|
||||
const query = Announcements.createQueryBuilder('announcement')
|
||||
.orderBy('"announcement"."createdAt"', 'DESC');
|
||||
@JsonController('/announcements')
|
||||
export class AnnouncementController {
|
||||
@Get() get() {
|
||||
const query = Announcements.createQueryBuilder('announcement')
|
||||
.orderBy('"announcement"."createdAt"', 'DESC');
|
||||
|
||||
return query.getMany();
|
||||
}
|
||||
return query.getMany();
|
||||
}
|
||||
|
||||
@OnUndefined(204)
|
||||
@Post() async post(@CurrentUser({ required: true }) user: IUser, @Body({required: true}) {title, body}: AnnounceCreate) {
|
||||
if (!user.isAdmin) {
|
||||
throw new BadRequestError('Not an Admin');
|
||||
}
|
||||
if (!title || !body) {
|
||||
throw new BadRequestError();
|
||||
}
|
||||
await Announcements.insert({
|
||||
createdAt: new Date(),
|
||||
title,
|
||||
body,
|
||||
});
|
||||
}
|
||||
@OnUndefined(204)
|
||||
@Post() async post(@CurrentUser({ required: true }) user: IUser, @Body({required: true}) {title, body}: AnnounceCreate) {
|
||||
if (!user.isAdmin) {
|
||||
throw new BadRequestError('Not an Admin');
|
||||
}
|
||||
if (!title || !body) {
|
||||
throw new BadRequestError();
|
||||
}
|
||||
await Announcements.insert({
|
||||
createdAt: new Date(),
|
||||
title,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
@OnUndefined(204)
|
||||
@Put() async update(@CurrentUser({ required: true }) user: IUser, @Body() {id, title, body}: AnnounceUpdate) {
|
||||
if (!user.isAdmin) {
|
||||
throw new BadRequestError('Not an Admin');
|
||||
}
|
||||
if (!id || !title || !body) {
|
||||
throw new BadRequestError();
|
||||
}
|
||||
if (!(await Announcements.findOne(id))) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
@OnUndefined(204)
|
||||
@Put() async update(@CurrentUser({ required: true }) user: IUser, @Body() {id, title, body}: AnnounceUpdate) {
|
||||
if (!user.isAdmin) {
|
||||
throw new BadRequestError('Not an Admin');
|
||||
}
|
||||
if (!id || !title || !body) {
|
||||
throw new BadRequestError();
|
||||
}
|
||||
if (!(await Announcements.findOne(id))) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
await Announcements.update(id, {
|
||||
title,
|
||||
body,
|
||||
});
|
||||
}
|
||||
await Announcements.update(id, {
|
||||
title,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
@OnUndefined(204)
|
||||
@Post('/like/:id') async like(@CurrentUser({ required: true }) user: IUser, @Param('id') id: string) {
|
||||
if (!user.isAdmin) {
|
||||
throw new BadRequestError('Not an Admin');
|
||||
}
|
||||
const idNumber = Number(id);
|
||||
if (isNaN(idNumber)) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
if (!id) {
|
||||
throw new BadRequestError();
|
||||
}
|
||||
@OnUndefined(204)
|
||||
@Post('/like/:id') async like(@CurrentUser({ required: true }) user: IUser, @Param('id') id: string) {
|
||||
if (!user.isAdmin) {
|
||||
throw new BadRequestError('Not an Admin');
|
||||
}
|
||||
const idNumber = Number(id);
|
||||
if (isNaN(idNumber)) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
if (!id) {
|
||||
throw new BadRequestError();
|
||||
}
|
||||
|
||||
const announcement = await Announcements.findOne(Number(idNumber));
|
||||
const announcement = await Announcements.findOne(Number(idNumber));
|
||||
|
||||
if (!announcement) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
if (!announcement) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
await Announcements.update(id, {
|
||||
like: announcement.like + 1,
|
||||
});
|
||||
await Announcements.update(id, {
|
||||
like: announcement.like + 1,
|
||||
});
|
||||
|
||||
return announcement.like + 1;
|
||||
}
|
||||
return announcement.like + 1;
|
||||
}
|
||||
|
||||
@Delete() async delete(@CurrentUser({ required: true }) user: IUser, @Body() {id}: IdProp) {
|
||||
if (!user.isAdmin) {
|
||||
throw new BadRequestError('Not an Admin');
|
||||
}
|
||||
@Delete() async delete(@CurrentUser({ required: true }) user: IUser, @Body() {id}: IdProp) {
|
||||
if (!user.isAdmin) {
|
||||
throw new BadRequestError('Not an Admin');
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
throw new BadRequestError();
|
||||
}
|
||||
if (!id) {
|
||||
throw new BadRequestError();
|
||||
}
|
||||
|
||||
await Announcements.delete(id);
|
||||
}
|
||||
await Announcements.delete(id);
|
||||
}
|
||||
|
||||
@Get('/:id') async getDetail(@Param('id') id: string) {
|
||||
const idNumber = Number(id);
|
||||
if (isNaN(idNumber)) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
const announcement = await Announcements.findOne(idNumber);
|
||||
if (!announcement) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
return announcement;
|
||||
}
|
||||
@Get('/:id') async getDetail(@Param('id') id: string) {
|
||||
const idNumber = Number(id);
|
||||
if (isNaN(idNumber)) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
const announcement = await Announcements.findOne(idNumber);
|
||||
if (!announcement) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
return announcement;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export class AnnounceCreate {
|
||||
title: string;
|
||||
body: string;
|
||||
title: string;
|
||||
body: string;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export class AnnounceUpdate {
|
||||
id: number;
|
||||
title: string;
|
||||
body: string;
|
||||
id: number;
|
||||
title: string;
|
||||
body: string;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
export class IdProp {
|
||||
id: number;
|
||||
id: number;
|
||||
}
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
import { IsIn, IsOptional } from 'class-validator';
|
||||
import { AlertMode, alertModes } from '../../../common/types/alert-mode';
|
||||
import { visibilities, Visibility } from '../../../common/types/visibility';
|
||||
import { AlertMode, alertModes } from '../../../common/types/alert-mode.js';
|
||||
import { visibilities, Visibility } from '../../../common/types/visibility.js';
|
||||
|
||||
export class UserSetting {
|
||||
@IsIn(alertModes)
|
||||
@IsOptional()
|
||||
alertMode?: AlertMode;
|
||||
@IsIn(alertModes)
|
||||
@IsOptional()
|
||||
alertMode?: AlertMode;
|
||||
|
||||
@IsIn(visibilities)
|
||||
@IsOptional()
|
||||
visibility?: Visibility;
|
||||
@IsIn(visibilities)
|
||||
@IsOptional()
|
||||
visibility?: Visibility;
|
||||
|
||||
@IsOptional()
|
||||
localOnly?: boolean;
|
||||
@IsOptional()
|
||||
localOnly?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
remoteFollowersOnly?: boolean;
|
||||
@IsOptional()
|
||||
remoteFollowersOnly?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
template?: string;
|
||||
@IsOptional()
|
||||
template?: string;
|
||||
|
||||
@IsOptional()
|
||||
useRanking?: boolean;
|
||||
@IsOptional()
|
||||
useRanking?: boolean;
|
||||
}
|
||||
|
|
13
src/backend/controllers/index.ts
Normal file
13
src/backend/controllers/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import {MetaController} from './meta.js';
|
||||
import {AdminController} from './admin.js';
|
||||
import {AnnouncementController} from './announcement.js';
|
||||
import {RankingController} from './ranking.js';
|
||||
import {SessionController} from './session.js';
|
||||
|
||||
export default [
|
||||
MetaController,
|
||||
AdminController,
|
||||
AnnouncementController,
|
||||
RankingController,
|
||||
SessionController,
|
||||
];
|
|
@ -3,19 +3,17 @@
|
|||
* @author Xeltica
|
||||
*/
|
||||
|
||||
import { readFile } from 'fs';
|
||||
import { Get, JsonController } from 'routing-controllers';
|
||||
import { promisify } from 'util';
|
||||
import { Meta } from '../../common/types/meta';
|
||||
import { currentTokenVersion } from '../const';
|
||||
import { Meta } from '../../common/types/meta.js';
|
||||
import { currentTokenVersion } from '../const.js';
|
||||
import { meta } from '../../config.js';
|
||||
|
||||
@JsonController('/meta')
|
||||
export class MetaController {
|
||||
@Get() async get(): Promise<Meta> {
|
||||
const {version} = JSON.parse(await promisify(readFile)(__dirname + '/../../meta.json', { encoding: 'utf-8'}));
|
||||
return {
|
||||
version,
|
||||
currentTokenVersion,
|
||||
};
|
||||
}
|
||||
@Get() async get(): Promise<Meta> {
|
||||
return {
|
||||
version: meta.version,
|
||||
currentTokenVersion,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,34 +4,34 @@
|
|||
*/
|
||||
|
||||
import { Get, JsonController, QueryParam } from 'routing-controllers';
|
||||
import { getRanking } from '../functions/ranking';
|
||||
import { getUserCount } from '../functions/users';
|
||||
import { getState } from '../store';
|
||||
import { getRanking } from '../functions/ranking.js';
|
||||
import { getUserCount } from '../functions/users.js';
|
||||
import { getState } from '../store.js';
|
||||
|
||||
@JsonController('/ranking')
|
||||
export class RankingController {
|
||||
@Get()
|
||||
async get(@QueryParam('limit', { required: false }) limit?: string) {
|
||||
return this.getResponse(getState().nowCalculating, limit ? Number(limit) : undefined);
|
||||
}
|
||||
@Get()
|
||||
async get(@QueryParam('limit', { required: false }) limit?: string) {
|
||||
return this.getResponse(getState().nowCalculating, limit ? Number(limit) : undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* DBに問い合わせてランキングを取得する
|
||||
* @param isCalculating 現在算出中かどうか
|
||||
* @param limit 何件取得するか
|
||||
* @returns ランキング
|
||||
*/
|
||||
private async getResponse(isCalculating: boolean, limit?: number) {
|
||||
const ranking = isCalculating ? [] : (await getRanking(limit)).map((u) => ({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
host: u.host,
|
||||
rating: u.rating,
|
||||
}));
|
||||
return {
|
||||
isCalculating,
|
||||
userCount: await getUserCount(),
|
||||
ranking,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* DBに問い合わせてランキングを取得する
|
||||
* @param isCalculating 現在算出中かどうか
|
||||
* @param limit 何件取得するか
|
||||
* @returns ランキング
|
||||
*/
|
||||
private async getResponse(isCalculating: boolean, limit?: number) {
|
||||
const ranking = isCalculating ? [] : (await getRanking(limit)).map((u) => ({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
host: u.host,
|
||||
rating: u.rating,
|
||||
}));
|
||||
return {
|
||||
isCalculating,
|
||||
userCount: await getUserCount(),
|
||||
ranking,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,45 +5,45 @@
|
|||
|
||||
import { Body, CurrentUser, Delete, Get, JsonController, OnUndefined, Post, Put } from 'routing-controllers';
|
||||
import { DeepPartial } from 'typeorm';
|
||||
import { getScores } from '../functions/get-scores';
|
||||
import { deleteUser, updateUser } from '../functions/users';
|
||||
import { User } from '../models/entities/user';
|
||||
import { sendAlert } from '../services/send-alert';
|
||||
import { UserSetting } from './body/user-setting';
|
||||
import { getScores } from '../functions/get-scores.js';
|
||||
import { deleteUser, updateUser } from '../functions/users.js';
|
||||
import { User } from '../models/entities/user.js';
|
||||
import { sendAlert } from '../services/send-alert.js';
|
||||
import { UserSetting } from './body/user-setting.js';
|
||||
|
||||
@JsonController('/session')
|
||||
export class SessionController {
|
||||
@Get() get(@CurrentUser({ required: true }) user: User) {
|
||||
return user;
|
||||
}
|
||||
@Get() get(@CurrentUser({ required: true }) user: User) {
|
||||
return user;
|
||||
}
|
||||
|
||||
@Get('/score')
|
||||
async getScore(@CurrentUser({ required: true }) user: User) {
|
||||
return getScores(user);
|
||||
}
|
||||
@Get('/score')
|
||||
async getScore(@CurrentUser({ required: true }) user: User) {
|
||||
return getScores(user);
|
||||
}
|
||||
|
||||
@OnUndefined(204)
|
||||
@Put() async updateSetting(@CurrentUser({ required: true }) user: User, @Body() setting: UserSetting) {
|
||||
const s: DeepPartial<User> = {};
|
||||
if (setting.alertMode != null) s.alertMode = setting.alertMode;
|
||||
if (setting.visibility != null) s.visibility = setting.visibility;
|
||||
if (setting.localOnly != null) s.localOnly = setting.localOnly;
|
||||
if (setting.remoteFollowersOnly != null) s.remoteFollowersOnly = setting.remoteFollowersOnly;
|
||||
if (setting.template !== undefined) s.template = setting.template;
|
||||
if (setting.useRanking !== undefined) s.useRanking = setting.useRanking;
|
||||
if (Object.keys(s).length === 0) return;
|
||||
await updateUser(user.username, user.host, s);
|
||||
}
|
||||
@OnUndefined(204)
|
||||
@Put() async updateSetting(@CurrentUser({ required: true }) user: User, @Body() setting: UserSetting) {
|
||||
const s: DeepPartial<User> = {};
|
||||
if (setting.alertMode != null) s.alertMode = setting.alertMode;
|
||||
if (setting.visibility != null) s.visibility = setting.visibility;
|
||||
if (setting.localOnly != null) s.localOnly = setting.localOnly;
|
||||
if (setting.remoteFollowersOnly != null) s.remoteFollowersOnly = setting.remoteFollowersOnly;
|
||||
if (setting.template !== undefined) s.template = setting.template;
|
||||
if (setting.useRanking !== undefined) s.useRanking = setting.useRanking;
|
||||
if (Object.keys(s).length === 0) return;
|
||||
await updateUser(user.username, user.host, s);
|
||||
}
|
||||
|
||||
@OnUndefined(204)
|
||||
@Post('/alert') async testAlert(@CurrentUser({ required: true }) user: User) {
|
||||
await sendAlert(user);
|
||||
}
|
||||
@OnUndefined(204)
|
||||
@Post('/alert') async testAlert(@CurrentUser({ required: true }) user: User) {
|
||||
await sendAlert(user);
|
||||
}
|
||||
|
||||
@OnUndefined(204)
|
||||
@Delete() async delete(@CurrentUser({ required: true }) user: User) {
|
||||
await deleteUser(user.username, user.host);
|
||||
}
|
||||
@OnUndefined(204)
|
||||
@Delete() async delete(@CurrentUser({ required: true }) user: User) {
|
||||
await deleteUser(user.username, user.host);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Context } from 'koa';
|
||||
import { ErrorCode } from '../common/types/error-code';
|
||||
import { ErrorCode } from '../common/types/error-code.js';
|
||||
|
||||
export const die = (ctx: Context, error: ErrorCode = 'other', status = 400): Promise<void> => {
|
||||
ctx.status = status;
|
||||
return ctx.render('frontend', { error });
|
||||
ctx.status = status;
|
||||
return ctx.render('frontend', { error });
|
||||
};
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {MisskeyError} from '../services/misskey';
|
||||
import {MisskeyError} from '../services/misskey.js';
|
||||
|
||||
export const errorToString = (e: Error) => {
|
||||
if (e instanceof MisskeyError) {
|
||||
return JSON.stringify(e.error);
|
||||
}
|
||||
return `${e.name}: ${e.message}\n${e.stack}`;
|
||||
if (e instanceof MisskeyError) {
|
||||
return JSON.stringify(e.error, null, ' ');
|
||||
}
|
||||
return `${e.name}: ${e.message}\n${e.stack}`;
|
||||
};
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import rndstr from 'rndstr';
|
||||
import { UsedToken } from '../models/entities/used-token';
|
||||
import { UsedTokens } from '../models';
|
||||
import { UsedToken } from '../models/entities/used-token.js';
|
||||
import { UsedTokens } from '../models/index.js';
|
||||
|
||||
/**
|
||||
* トークンを生成します
|
||||
*/
|
||||
export const genToken = async (): Promise<string> => {
|
||||
let used: UsedToken | undefined = undefined;
|
||||
let token: string;
|
||||
do {
|
||||
token = rndstr(32);
|
||||
used = await UsedTokens.findOne({ token });
|
||||
} while (used !== undefined);
|
||||
return token;
|
||||
let used: UsedToken | undefined = undefined;
|
||||
let token: string;
|
||||
do {
|
||||
token = rndstr(32);
|
||||
used = await UsedTokens.findOne({ token });
|
||||
} while (used !== undefined);
|
||||
return token;
|
||||
};
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { User } from '../models/entities/user';
|
||||
import { toSignedString } from '../../common/functions/to-signed-string';
|
||||
import {Count} from '../models/count';
|
||||
import {api} from '../services/misskey';
|
||||
import {Score} from '../../common/types/score';
|
||||
import {MiUser} from './update-score';
|
||||
import { User } from '../models/entities/user.js';
|
||||
import { toSignedString } from '../../common/functions/to-signed-string.js';
|
||||
import {Count} from '../models/count.js';
|
||||
import {api} from '../services/misskey.js';
|
||||
import {Score} from '../../common/types/score.js';
|
||||
import {MiUser} from './update-score.js';
|
||||
|
||||
/**
|
||||
* ユーザーのスコアを取得します。
|
||||
|
@ -11,15 +11,15 @@ import {MiUser} from './update-score';
|
|||
* @returns ユーザーのスコア
|
||||
*/
|
||||
export const getScores = async (user: User): Promise<Score> => {
|
||||
// TODO 毎回取ってくるのも微妙なので、ある程度キャッシュしたいかも
|
||||
const miUser = await api<MiUser>(user.host, 'users/show', { username: user.username }, user.token);
|
||||
// TODO 毎回取ってくるのも微妙なので、ある程度キャッシュしたいかも
|
||||
const miUser = await api<MiUser>(user.host, 'users/show', { username: user.username }, user.token);
|
||||
|
||||
return {
|
||||
notesCount: miUser.notesCount,
|
||||
followingCount: miUser.followingCount,
|
||||
followersCount: miUser.followersCount,
|
||||
...getDelta(user, miUser),
|
||||
};
|
||||
return {
|
||||
notesCount: miUser.notesCount,
|
||||
followingCount: miUser.followingCount,
|
||||
followersCount: miUser.followersCount,
|
||||
...getDelta(user, miUser),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -29,9 +29,9 @@ export const getScores = async (user: User): Promise<Score> => {
|
|||
* @returns ユーザーのスコア差分
|
||||
*/
|
||||
export const getDelta = (user: User, count: Count) => {
|
||||
return {
|
||||
notesDelta: toSignedString(count.notesCount - user.prevNotesCount),
|
||||
followingDelta: toSignedString(count.followingCount - user.prevFollowingCount),
|
||||
followersDelta: toSignedString(count.followersCount - user.prevFollowersCount),
|
||||
};
|
||||
return {
|
||||
notesDelta: toSignedString(count.notesCount - user.prevNotesCount),
|
||||
followingDelta: toSignedString(count.followingCount - user.prevFollowingCount),
|
||||
followersDelta: toSignedString(count.followersCount - user.prevFollowersCount),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Users } from '../models';
|
||||
import { User } from '../models/entities/user';
|
||||
import { Users } from '../models/index.js';
|
||||
import { User } from '../models/entities/user.js';
|
||||
|
||||
/**
|
||||
* ミス廃ランキングを取得する
|
||||
|
@ -7,15 +7,15 @@ import { User } from '../models/entities/user';
|
|||
* @returns ミス廃ランキング
|
||||
*/
|
||||
export const getRanking = async (limit?: number | null): Promise<User[]> => {
|
||||
const query = Users.createQueryBuilder('user')
|
||||
.where('"user"."useRanking" IS TRUE')
|
||||
.andWhere('"user"."bannedFromRanking" IS NOT TRUE')
|
||||
.andWhere('"user"."rating" <> \'NaN\'')
|
||||
.orderBy('"user".rating', 'DESC');
|
||||
const query = Users.createQueryBuilder('user')
|
||||
.where('"user"."useRanking" IS TRUE')
|
||||
.andWhere('"user"."bannedFromRanking" IS NOT TRUE')
|
||||
.andWhere('"user"."rating" <> \'NaN\'')
|
||||
.orderBy('"user".rating', 'DESC');
|
||||
|
||||
if (limit) {
|
||||
query.limit(limit);
|
||||
}
|
||||
if (limit) {
|
||||
query.limit(limit);
|
||||
}
|
||||
|
||||
return await query.getMany();
|
||||
return await query.getMany();
|
||||
};
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import dayjs from 'dayjs';
|
||||
|
||||
import { User } from '../models/entities/user';
|
||||
import { updateUser } from './users';
|
||||
import { MiUser } from './update-score';
|
||||
import { User } from '../models/entities/user.js';
|
||||
import { updateUser } from './users.js';
|
||||
import { MiUser } from './update-score.js';
|
||||
|
||||
/**
|
||||
* ユーザーのレーティングを更新します
|
||||
|
@ -10,9 +10,9 @@ import { MiUser } from './update-score';
|
|||
* @param miUser Misskeyのユーザー
|
||||
*/
|
||||
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,
|
||||
});
|
||||
const elapsedDays = dayjs().diff(dayjs(miUser.createdAt), 'd') + 1;
|
||||
await updateUser(user.username, user.host, {
|
||||
prevRating: user.rating,
|
||||
rating: miUser.notesCount / elapsedDays,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { User } from '../models/entities/user';
|
||||
import { updateUser } from './users';
|
||||
import {Count} from '../models/count';
|
||||
import { User } from '../models/entities/user.js';
|
||||
import { updateUser } from './users.js';
|
||||
import {Count} from '../models/count.js';
|
||||
|
||||
/**
|
||||
* Misskeyのユーザーモデル
|
||||
*/
|
||||
export type MiUser = {
|
||||
notesCount: number,
|
||||
followingCount: number,
|
||||
followersCount: number,
|
||||
createdAt: string,
|
||||
notesCount: number,
|
||||
followingCount: number,
|
||||
followersCount: number,
|
||||
createdAt: string,
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -18,9 +18,9 @@ export type MiUser = {
|
|||
* @param count 統計
|
||||
*/
|
||||
export const updateScore = async (user: User, count: Count): Promise<void> => {
|
||||
await updateUser(user.username, user.host, {
|
||||
prevNotesCount: count.notesCount ?? 0,
|
||||
prevFollowingCount: count.followingCount ?? 0,
|
||||
prevFollowersCount: count.followersCount ?? 0,
|
||||
});
|
||||
await updateUser(user.username, user.host, {
|
||||
prevNotesCount: count.notesCount ?? 0,
|
||||
prevFollowingCount: count.followingCount ?? 0,
|
||||
prevFollowersCount: count.followersCount ?? 0,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
import { User } from '../models/entities/user';
|
||||
import { Users } from '../models';
|
||||
import { User } from '../models/entities/user.js';
|
||||
import { Users } from '../models/index.js';
|
||||
import { DeepPartial } from 'typeorm';
|
||||
import { genToken } from './gen-token';
|
||||
import { IUser } from '../../common/types/user';
|
||||
import { config } from '../../config';
|
||||
import { currentTokenVersion } from '../const';
|
||||
import { genToken } from './gen-token.js';
|
||||
import { IUser } from '../../common/types/user.js';
|
||||
import { config } from '../../config.js';
|
||||
import { currentTokenVersion } from '../const.js';
|
||||
|
||||
/**
|
||||
* IUser インターフェイスに変換します。
|
||||
*/
|
||||
const packUser = (user: User | undefined): IUser | undefined => {
|
||||
if (!user) return undefined;
|
||||
const { username: adminName, host: adminHost } = config.admin;
|
||||
if (!user) return undefined;
|
||||
const { username: adminName, host: adminHost } = config.admin;
|
||||
|
||||
return {
|
||||
...user,
|
||||
isAdmin: adminName === user.username && adminHost === user.host,
|
||||
};
|
||||
return {
|
||||
...user,
|
||||
isAdmin: adminName === user.username && adminHost === user.host,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -26,7 +26,7 @@ const packUser = (user: User | undefined): IUser | undefined => {
|
|||
* @returns ユーザー
|
||||
*/
|
||||
export const getUser = (username: string, host: string): Promise<IUser | undefined> => {
|
||||
return Users.findOne({ username, host }).then(packUser);
|
||||
return Users.findOne({ username, host }).then(packUser);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -35,13 +35,13 @@ export const getUser = (username: string, host: string): Promise<IUser | undefin
|
|||
* @returns ミス廃トークン
|
||||
*/
|
||||
export const updateUsersToolsToken = async (user: User | User['id']): Promise<string> => {
|
||||
const id = typeof user === 'number'
|
||||
? user
|
||||
: user.id;
|
||||
const id = typeof user === 'number'
|
||||
? user
|
||||
: user.id;
|
||||
|
||||
const misshaiToken = await genToken();
|
||||
Users.update(id, { misshaiToken });
|
||||
return misshaiToken;
|
||||
const misshaiToken = await genToken();
|
||||
Users.update(id, { misshaiToken });
|
||||
return misshaiToken;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -50,7 +50,7 @@ export const updateUsersToolsToken = async (user: User | User['id']): Promise<st
|
|||
* @returns ユーザー
|
||||
*/
|
||||
export const getUserByToolsToken = (token: string): Promise<IUser | undefined> => {
|
||||
return Users.findOne({ misshaiToken: token }).then(packUser);
|
||||
return Users.findOne({ misshaiToken: token }).then(packUser);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -60,13 +60,13 @@ export const getUserByToolsToken = (token: string): Promise<IUser | undefined> =
|
|||
* @param token トークン
|
||||
*/
|
||||
export const upsertUser = async (username: string, host: string, token: string): Promise<void> => {
|
||||
const u = await getUser(username, host);
|
||||
if (u) {
|
||||
await Users.update(u.id, { token, tokenVersion: currentTokenVersion });
|
||||
} else {
|
||||
const result = await Users.save({ username, host, token, tokenVersion: currentTokenVersion });
|
||||
await updateUsersToolsToken(result.id);
|
||||
}
|
||||
const u = await getUser(username, host);
|
||||
if (u) {
|
||||
await Users.update(u.id, { token, tokenVersion: currentTokenVersion });
|
||||
} else {
|
||||
const result = await Users.save({ username, host, token, tokenVersion: currentTokenVersion });
|
||||
await updateUsersToolsToken(result.id);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -76,7 +76,7 @@ export const upsertUser = async (username: string, host: string, token: string):
|
|||
* @param record 既存のユーザー情報
|
||||
*/
|
||||
export const updateUser = async (username: string, host: string, record: DeepPartial<User>): Promise<void> => {
|
||||
await Users.update({ username, host }, record);
|
||||
await Users.update({ username, host }, record);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -85,7 +85,7 @@ export const updateUser = async (username: string, host: string, record: DeepPar
|
|||
* @param host ホスト名
|
||||
*/
|
||||
export const deleteUser = async (username: string, host: string): Promise<void> => {
|
||||
await Users.delete({ username, host });
|
||||
await Users.delete({ username, host });
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -93,5 +93,5 @@ export const deleteUser = async (username: string, host: string): Promise<void>
|
|||
* @returns ユーザー数
|
||||
*/
|
||||
export const getUserCount = (): Promise<number> => {
|
||||
return Users.count();
|
||||
return Users.count();
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export interface Count
|
||||
{
|
||||
notesCount: number;
|
||||
followingCount: number;
|
||||
followersCount: number;
|
||||
notesCount: number;
|
||||
followingCount: number;
|
||||
followersCount: number;
|
||||
}
|
||||
|
|
|
@ -1,31 +1,31 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
import { IAnnouncement } from '../../../common/types/announcement';
|
||||
import {Entity, PrimaryGeneratedColumn, Column} from 'typeorm';
|
||||
import {IAnnouncement} from '../../../common/types/announcement.js';
|
||||
|
||||
@Entity()
|
||||
export class Announcement implements IAnnouncement {
|
||||
@PrimaryGeneratedColumn()
|
||||
public id: number;
|
||||
@PrimaryGeneratedColumn()
|
||||
public id: number;
|
||||
|
||||
@Column({
|
||||
type: 'timestamp without time zone',
|
||||
})
|
||||
public createdAt: Date;
|
||||
@Column({
|
||||
type: 'timestamp without time zone',
|
||||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 128,
|
||||
})
|
||||
public title: string;
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 128,
|
||||
})
|
||||
public title: string;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 8192,
|
||||
})
|
||||
public body: string;
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 8192,
|
||||
})
|
||||
public body: string;
|
||||
|
||||
@Column({
|
||||
type: 'integer',
|
||||
default: 0,
|
||||
})
|
||||
public like: number;
|
||||
@Column({
|
||||
type: 'integer',
|
||||
default: 0,
|
||||
})
|
||||
public like: number;
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ import { Entity, Index, PrimaryColumn } from 'typeorm';
|
|||
@Entity()
|
||||
@Index([ 'token' ], { unique: true })
|
||||
export class UsedToken {
|
||||
@PrimaryColumn({
|
||||
type: 'varchar'
|
||||
})
|
||||
public token: string;
|
||||
}
|
||||
@PrimaryColumn({
|
||||
type: 'varchar'
|
||||
})
|
||||
public token: string;
|
||||
}
|
||||
|
|
|
@ -1,114 +1,114 @@
|
|||
import { Entity, Column, PrimaryGeneratedColumn, Index } from 'typeorm';
|
||||
import { AlertMode, alertModes } from '../../../common/types/alert-mode';
|
||||
import { visibilities, Visibility } from '../../../common/types/visibility';
|
||||
import { IUser } from '../../../common/types/user';
|
||||
import { AlertMode, alertModes } from '../../../common/types/alert-mode.js';
|
||||
import { visibilities, Visibility } from '../../../common/types/visibility.js';
|
||||
import { IUser } from '../../../common/types/user.js';
|
||||
|
||||
@Entity()
|
||||
@Index(['username', 'host'], { unique: true })
|
||||
export class User implements IUser {
|
||||
@PrimaryGeneratedColumn()
|
||||
public id: number;
|
||||
@PrimaryGeneratedColumn()
|
||||
public id: number;
|
||||
|
||||
@Column({
|
||||
type: 'varchar'
|
||||
})
|
||||
public username: string;
|
||||
@Column({
|
||||
type: 'varchar'
|
||||
})
|
||||
public username: string;
|
||||
|
||||
@Column({
|
||||
type: 'varchar'
|
||||
})
|
||||
public host: string;
|
||||
@Column({
|
||||
type: 'varchar'
|
||||
})
|
||||
public host: string;
|
||||
|
||||
@Column({
|
||||
type: 'varchar'
|
||||
})
|
||||
public token: string;
|
||||
@Column({
|
||||
type: 'varchar'
|
||||
})
|
||||
public token: string;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
default: ''
|
||||
})
|
||||
public misshaiToken: string;
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
default: ''
|
||||
})
|
||||
public misshaiToken: string;
|
||||
|
||||
@Column({
|
||||
type: 'integer',
|
||||
default: 0,
|
||||
})
|
||||
public prevNotesCount: number;
|
||||
@Column({
|
||||
type: 'integer',
|
||||
default: 0,
|
||||
})
|
||||
public prevNotesCount: number;
|
||||
|
||||
@Column({
|
||||
type: 'integer',
|
||||
default: 0,
|
||||
})
|
||||
public prevFollowingCount: number;
|
||||
@Column({
|
||||
type: 'integer',
|
||||
default: 0,
|
||||
})
|
||||
public prevFollowingCount: number;
|
||||
|
||||
@Column({
|
||||
type: 'integer',
|
||||
default: 0,
|
||||
})
|
||||
public prevFollowersCount: number;
|
||||
@Column({
|
||||
type: 'integer',
|
||||
default: 0,
|
||||
})
|
||||
public prevFollowersCount: number;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: alertModes,
|
||||
default: 'notification'
|
||||
})
|
||||
public alertMode: AlertMode;
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: alertModes,
|
||||
default: 'notification'
|
||||
})
|
||||
public alertMode: AlertMode;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: visibilities,
|
||||
default: 'home',
|
||||
})
|
||||
public visibility: Visibility;
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: visibilities,
|
||||
default: 'home',
|
||||
})
|
||||
public visibility: Visibility;
|
||||
|
||||
@Column({
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
public localOnly: boolean;
|
||||
@Column({
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
public localOnly: boolean;
|
||||
|
||||
@Column({
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
public remoteFollowersOnly: boolean;
|
||||
@Column({
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
public remoteFollowersOnly: boolean;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 1024,
|
||||
nullable: true,
|
||||
})
|
||||
public template: string | null;
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 1024,
|
||||
nullable: true,
|
||||
})
|
||||
public template: string | null;
|
||||
|
||||
@Column({
|
||||
type: 'real',
|
||||
default: 0,
|
||||
})
|
||||
public prevRating: number;
|
||||
@Column({
|
||||
type: 'real',
|
||||
default: 0,
|
||||
})
|
||||
public prevRating: number;
|
||||
|
||||
@Column({
|
||||
type: 'real',
|
||||
default: 0,
|
||||
})
|
||||
public rating: number;
|
||||
@Column({
|
||||
type: 'real',
|
||||
default: 0,
|
||||
})
|
||||
public rating: number;
|
||||
|
||||
@Column({
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
public useRanking: boolean;
|
||||
@Column({
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
public useRanking: boolean;
|
||||
|
||||
@Column({
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
public bannedFromRanking: boolean;
|
||||
@Column({
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
public bannedFromRanking: boolean;
|
||||
|
||||
@Column({
|
||||
type: 'integer',
|
||||
default: 1,
|
||||
comment: 'Misskey API トークンのバージョン。現行と違う場合はアップデートを要求する',
|
||||
})
|
||||
public tokenVersion: number;
|
||||
@Column({
|
||||
type: 'integer',
|
||||
default: 1,
|
||||
comment: 'Misskey API トークンのバージョン。現行と違う場合はアップデートを要求する',
|
||||
})
|
||||
public tokenVersion: number;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { User } from './entities/user';
|
||||
import { UsedToken } from './entities/used-token';
|
||||
import { User } from './entities/user.js';
|
||||
import { UsedToken } from './entities/used-token.js';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { Announcement } from './entities/announcement';
|
||||
import { Announcement } from './entities/announcement.js';
|
||||
|
||||
export const Users = getRepository(User);
|
||||
export const UsedTokens = getRepository(UsedToken);
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import views from 'koa-views';
|
||||
import { version } from '../meta.json';
|
||||
import path from 'path';
|
||||
import url from 'url';
|
||||
|
||||
import { meta } from '../config.js';
|
||||
|
||||
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
||||
|
||||
export const render = views(__dirname + '/views', {
|
||||
extension: 'pug',
|
||||
options: { version },
|
||||
extension: 'pug',
|
||||
options: { version: meta.version },
|
||||
});
|
||||
|
|
|
@ -8,12 +8,16 @@ import ms from 'ms';
|
|||
import striptags from 'striptags';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
|
||||
import { config } from '../config';
|
||||
import { upsertUser, getUser, updateUser } from './functions/users';
|
||||
import { api } from './services/misskey';
|
||||
import { die } from './die';
|
||||
import { misskeyAppInfo } from './const';
|
||||
import { Announcements } from './models';
|
||||
import { config } from '../config.js';
|
||||
import { upsertUser, getUser, updateUser } from './functions/users.js';
|
||||
import { api } from './services/misskey.js';
|
||||
import { die } from './die.js';
|
||||
import { misskeyAppInfo } from './const.js';
|
||||
import { Announcements } from './models/index.js';
|
||||
import path from 'path';
|
||||
import url from 'url';
|
||||
|
||||
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
||||
|
||||
export const router = new Router<DefaultState, Context>();
|
||||
|
||||
|
@ -23,175 +27,175 @@ const tokenSecretCache: Record<string, string> = {};
|
|||
const md = new MarkdownIt();
|
||||
|
||||
router.get('/login', async ctx => {
|
||||
let host = ctx.query.host as string | undefined;
|
||||
if (!host) {
|
||||
await die(ctx, 'invalidParamater');
|
||||
return;
|
||||
}
|
||||
let host = ctx.query.host as string | undefined;
|
||||
if (!host) {
|
||||
await die(ctx, 'invalidParamater');
|
||||
return;
|
||||
}
|
||||
|
||||
// http://, https://を潰す
|
||||
host = host.trim().replace(/^https?:\/\//g, '').replace(/\/+/g, '');
|
||||
// http://, https://を潰す
|
||||
host = host.trim().replace(/^https?:\/\//g, '').replace(/\/+/g, '');
|
||||
|
||||
const meta = await api<{ name: string, uri: string, version: string, features: Record<string, boolean | undefined> }>(host, 'meta', {}).catch(async e => {
|
||||
if (!(e instanceof Error && e.name === 'Error')) throw e;
|
||||
await die(ctx, 'hostNotFound');
|
||||
});
|
||||
const meta = await api<{ name: string, uri: string, version: string, features: Record<string, boolean | undefined> }>(host, 'meta', {}).catch(async e => {
|
||||
if (!(e instanceof Error && e.name === 'Error')) throw e;
|
||||
await die(ctx, 'hostNotFound');
|
||||
});
|
||||
|
||||
// NOTE: catchが呼ばれた場合はvoidとなるためundefinedのはず
|
||||
if (typeof meta === 'undefined') return;
|
||||
// NOTE: catchが呼ばれた場合はvoidとなるためundefinedのはず
|
||||
if (typeof meta === 'undefined') return;
|
||||
|
||||
if (typeof meta !== 'object') {
|
||||
await die(ctx, 'other');
|
||||
return;
|
||||
}
|
||||
if (typeof meta !== 'object') {
|
||||
await die(ctx, 'other');
|
||||
return;
|
||||
}
|
||||
|
||||
if (meta.version.includes('hitori')) {
|
||||
await die(ctx, 'hitorisskeyIsDenied');
|
||||
return;
|
||||
}
|
||||
if (meta.version.includes('hitori')) {
|
||||
await die(ctx, 'hitorisskeyIsDenied');
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE:
|
||||
// 環境によってはアクセスしたドメインとMisskeyにおけるhostが異なるケースがある
|
||||
// そういったインスタンスにおいてアカウントの不整合が生じるため、
|
||||
// APIから戻ってきたホスト名を正しいものとして、改めて正規化する
|
||||
host = meta.uri.replace(/^https?:\/\//g, '').replace(/\/+/g, '').trim();
|
||||
// NOTE:
|
||||
// 環境によってはアクセスしたドメインとMisskeyにおけるhostが異なるケースがある
|
||||
// そういったインスタンスにおいてアカウントの不整合が生じるため、
|
||||
// APIから戻ってきたホスト名を正しいものとして、改めて正規化する
|
||||
host = meta.uri.replace(/^https?:\/\//g, '').replace(/\/+/g, '').trim();
|
||||
|
||||
const { name, permission, description } = misskeyAppInfo;
|
||||
const { name, permission, description } = misskeyAppInfo;
|
||||
|
||||
if (meta.features.miauth) {
|
||||
// MiAuthを使用する
|
||||
const callback = encodeURI(`${config.url}/miauth`);
|
||||
if (meta.features.miauth) {
|
||||
// MiAuthを使用する
|
||||
const callback = encodeURI(`${config.url}/miauth`);
|
||||
|
||||
const session = uuid();
|
||||
const url = `https://${host}/miauth/${session}?name=${encodeURI(name)}&callback=${encodeURI(callback)}&permission=${encodeURI(permission.join(','))}`;
|
||||
sessionHostCache[session] = host;
|
||||
const session = uuid();
|
||||
const url = `https://${host}/miauth/${session}?name=${encodeURI(name)}&callback=${encodeURI(callback)}&permission=${encodeURI(permission.join(','))}`;
|
||||
sessionHostCache[session] = host;
|
||||
|
||||
ctx.redirect(url);
|
||||
} else {
|
||||
// 旧型認証を使用する
|
||||
const callbackUrl = encodeURI(`${config.url}/legacy-auth`);
|
||||
ctx.redirect(url);
|
||||
} else {
|
||||
// 旧型認証を使用する
|
||||
const callbackUrl = encodeURI(`${config.url}/legacy-auth`);
|
||||
|
||||
const { secret } = await api<{ secret: string }>(host, 'app/create', {
|
||||
name, description, permission, callbackUrl,
|
||||
});
|
||||
const { secret } = await api<{ secret: string }>(host, 'app/create', {
|
||||
name, description, permission, callbackUrl,
|
||||
});
|
||||
|
||||
const { token, url } = await api<{ token: string, url: string }>(host, 'auth/session/generate', {
|
||||
appSecret: secret
|
||||
});
|
||||
const { token, url } = await api<{ token: string, url: string }>(host, 'auth/session/generate', {
|
||||
appSecret: secret
|
||||
});
|
||||
|
||||
sessionHostCache[token] = host;
|
||||
tokenSecretCache[token] = secret;
|
||||
sessionHostCache[token] = host;
|
||||
tokenSecretCache[token] = secret;
|
||||
|
||||
ctx.redirect(url);
|
||||
}
|
||||
ctx.redirect(url);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/teapot', async ctx => {
|
||||
await die(ctx, 'teapot', 418);
|
||||
await die(ctx, 'teapot', 418);
|
||||
});
|
||||
|
||||
router.get('/miauth', async ctx => {
|
||||
const session = ctx.query.session as string | undefined;
|
||||
if (!session) {
|
||||
await die(ctx, 'sessionRequired');
|
||||
return;
|
||||
}
|
||||
const host = sessionHostCache[session];
|
||||
delete sessionHostCache[session];
|
||||
if (!host) {
|
||||
await die(ctx);
|
||||
console.error('host is null or undefined');
|
||||
return;
|
||||
}
|
||||
const session = ctx.query.session as string | undefined;
|
||||
if (!session) {
|
||||
await die(ctx, 'sessionRequired');
|
||||
return;
|
||||
}
|
||||
const host = sessionHostCache[session];
|
||||
delete sessionHostCache[session];
|
||||
if (!host) {
|
||||
await die(ctx);
|
||||
console.error('host is null or undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `https://${host}/api/miauth/${session}/check`;
|
||||
const res = await axios.post(url, {});
|
||||
const { token, user } = res.data;
|
||||
const url = `https://${host}/api/miauth/${session}/check`;
|
||||
const res = await axios.post(url, {});
|
||||
const { token, user } = res.data;
|
||||
|
||||
if (!token || !user) {
|
||||
await die(ctx);
|
||||
if (!token) console.error('token is null or undefined');
|
||||
if (!user) console.error('user is null or undefined');
|
||||
return;
|
||||
}
|
||||
if (!token || !user) {
|
||||
await die(ctx);
|
||||
if (!token) console.error('token is null or undefined');
|
||||
if (!user) console.error('user is null or undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
await login(ctx, user, host, token);
|
||||
await login(ctx, user, host, token);
|
||||
|
||||
});
|
||||
|
||||
router.get('/legacy-auth', async ctx => {
|
||||
const token = ctx.query.token as string | undefined;
|
||||
if (!token) {
|
||||
await die(ctx, 'tokenRequired');
|
||||
return;
|
||||
}
|
||||
const host = sessionHostCache[token];
|
||||
delete sessionHostCache[token];
|
||||
if (!host) {
|
||||
await die(ctx);
|
||||
return;
|
||||
}
|
||||
const appSecret = tokenSecretCache[token];
|
||||
delete tokenSecretCache[token];
|
||||
if (!appSecret) {
|
||||
await die(ctx);
|
||||
return;
|
||||
}
|
||||
const token = ctx.query.token as string | undefined;
|
||||
if (!token) {
|
||||
await die(ctx, 'tokenRequired');
|
||||
return;
|
||||
}
|
||||
const host = sessionHostCache[token];
|
||||
delete sessionHostCache[token];
|
||||
if (!host) {
|
||||
await die(ctx);
|
||||
return;
|
||||
}
|
||||
const appSecret = tokenSecretCache[token];
|
||||
delete tokenSecretCache[token];
|
||||
if (!appSecret) {
|
||||
await die(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
const { accessToken, user } = await api<{ accessToken: string, user: Record<string, unknown> }>(host, 'auth/session/userkey', {
|
||||
appSecret, token,
|
||||
});
|
||||
const i = crypto.createHash('sha256').update(accessToken + appSecret, 'utf8').digest('hex');
|
||||
const { accessToken, user } = await api<{ accessToken: string, user: Record<string, unknown> }>(host, 'auth/session/userkey', {
|
||||
appSecret, token,
|
||||
});
|
||||
const i = crypto.createHash('sha256').update(accessToken + appSecret, 'utf8').digest('hex');
|
||||
|
||||
await login(ctx, user, host, i);
|
||||
await login(ctx, user, host, i);
|
||||
});
|
||||
|
||||
router.get('/assets/(.*)', async ctx => {
|
||||
await koaSend(ctx as any, ctx.path.replace('/assets/', ''), {
|
||||
root: `${__dirname}/../assets/`,
|
||||
maxage: process.env.NODE_ENV !== 'production' ? 0 : ms('7 days'),
|
||||
});
|
||||
await koaSend(ctx as any, ctx.path.replace('/assets/', ''), {
|
||||
root: `${__dirname}/../assets/`,
|
||||
maxage: process.env.NODE_ENV !== 'production' ? 0 : ms('7 days'),
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/api(.*)', async (ctx, next) => {
|
||||
next();
|
||||
next();
|
||||
});
|
||||
|
||||
router.get('/announcements/:id', async (ctx) => {
|
||||
const a = await Announcements.findOne(ctx.params.id);
|
||||
const stripped = striptags(md.render(a?.body ?? '').replace(/\n/g, ' '));
|
||||
await ctx.render('frontend', a ? {
|
||||
t: a.title,
|
||||
d: stripped.length > 80 ? stripped.substring(0, 80) + '…' : stripped,
|
||||
} : null);
|
||||
const a = await Announcements.findOne(ctx.params.id);
|
||||
const stripped = striptags(md.render(a?.body ?? '').replace(/\n/g, ' '));
|
||||
await ctx.render('frontend', a ? {
|
||||
t: a.title,
|
||||
d: stripped.length > 80 ? stripped.substring(0, 80) + '…' : stripped,
|
||||
} : null);
|
||||
});
|
||||
|
||||
router.get('/__rescue__', async(ctx) => {
|
||||
await ctx.render('rescue');
|
||||
await ctx.render('rescue');
|
||||
});
|
||||
|
||||
router.get('(.*)', async (ctx) => {
|
||||
await ctx.render('frontend');
|
||||
await ctx.render('frontend');
|
||||
});
|
||||
|
||||
async function login(ctx: Context, user: Record<string, unknown>, host: string, token: string) {
|
||||
const isNewcomer = !(await getUser(user.username as string, host));
|
||||
await upsertUser(user.username as string, host, token);
|
||||
const isNewcomer = !(await getUser(user.username as string, host));
|
||||
await upsertUser(user.username as string, host, token);
|
||||
|
||||
const u = await getUser(user.username as string, host);
|
||||
const u = await getUser(user.username as string, host);
|
||||
|
||||
if (!u) {
|
||||
await die(ctx);
|
||||
return;
|
||||
}
|
||||
if (!u) {
|
||||
await die(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNewcomer) {
|
||||
await updateUser(u.username, u.host, {
|
||||
prevNotesCount: user.notesCount as number ?? 0,
|
||||
prevFollowingCount: user.followingCount as number ?? 0,
|
||||
prevFollowersCount: user.followersCount as number ?? 0,
|
||||
});
|
||||
}
|
||||
if (isNewcomer) {
|
||||
await updateUser(u.username, u.host, {
|
||||
prevNotesCount: user.notesCount as number ?? 0,
|
||||
prevFollowingCount: user.followingCount as number ?? 0,
|
||||
prevFollowersCount: user.followersCount as number ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.render('frontend', { token: u.misshaiToken });
|
||||
await ctx.render('frontend', { token: u.misshaiToken });
|
||||
}
|
||||
|
|
|
@ -1,45 +1,44 @@
|
|||
import Koa from 'koa';
|
||||
import bodyParser from 'koa-bodyparser';
|
||||
import { Action, useKoaServer } from 'routing-controllers';
|
||||
import {Action, useKoaServer} from 'routing-controllers';
|
||||
|
||||
import { config } from '../config';
|
||||
import { render } from './render';
|
||||
import { router } from './router';
|
||||
import { getUserByToolsToken } from './functions/users';
|
||||
import { version } from '../meta.json';
|
||||
import {config, meta} from '../config.js';
|
||||
import {render} from './render.js';
|
||||
import {router} from './router.js';
|
||||
import {getUserByToolsToken} from './functions/users.js';
|
||||
import controllers from './controllers/index.js';
|
||||
|
||||
import 'reflect-metadata';
|
||||
|
||||
export default (): void => {
|
||||
const app = new Koa();
|
||||
const app = new Koa();
|
||||
|
||||
console.log('Misskey Tools v' + version);
|
||||
console.log('Misskey Tools v' + meta.version);
|
||||
|
||||
console.log('Initializing DB connection...');
|
||||
console.log('Initializing DB connection...');
|
||||
|
||||
app.use(render);
|
||||
app.use(bodyParser());
|
||||
app.use(render);
|
||||
app.use(bodyParser());
|
||||
|
||||
useKoaServer(app, {
|
||||
controllers: [__dirname + '/controllers/**/*{.ts,.js}'],
|
||||
routePrefix: '/api/v1',
|
||||
classTransformer: true,
|
||||
validation: true,
|
||||
currentUserChecker: async ({ request }: Action) => {
|
||||
const { authorization } = request.header;
|
||||
if (!authorization || !authorization.startsWith('Bearer ')) return null;
|
||||
useKoaServer(app, {
|
||||
controllers,
|
||||
routePrefix: '/api/v1',
|
||||
classTransformer: true,
|
||||
validation: true,
|
||||
currentUserChecker: async ({ request }: Action) => {
|
||||
const { authorization } = request.header;
|
||||
if (!authorization || !authorization.startsWith('Bearer ')) return null;
|
||||
|
||||
const token = authorization.split(' ')[1].trim();
|
||||
const user = await getUserByToolsToken(token);
|
||||
return user;
|
||||
},
|
||||
});
|
||||
const token = authorization.split(' ')[1].trim();
|
||||
return await getUserByToolsToken(token);
|
||||
},
|
||||
});
|
||||
|
||||
app.use(router.routes());
|
||||
app.use(router.allowedMethods());
|
||||
app.use(router.routes());
|
||||
app.use(router.allowedMethods());
|
||||
|
||||
console.log(`listening port ${config.port}...`);
|
||||
console.log('App launched!');
|
||||
console.log(`listening port ${config.port}...`);
|
||||
console.log('App launched!');
|
||||
|
||||
app.listen(config.port || 3000);
|
||||
app.listen(config.port || 3000);
|
||||
};
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { getConnection, createConnection, Connection } from 'typeorm';
|
||||
import { config } from '../../config';
|
||||
import { User } from '../models/entities/user';
|
||||
import { UsedToken } from '../models/entities/used-token';
|
||||
import { Announcement } from '../models/entities/announcement';
|
||||
import { config } from '../../config.js';
|
||||
import { User } from '../models/entities/user.js';
|
||||
import { UsedToken } from '../models/entities/used-token.js';
|
||||
import { Announcement } from '../models/entities/announcement.js';
|
||||
|
||||
export const entities = [
|
||||
User,
|
||||
UsedToken,
|
||||
Announcement,
|
||||
User,
|
||||
UsedToken,
|
||||
Announcement,
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -16,26 +16,26 @@ export const entities = [
|
|||
* @returns 取得または作成したDBコネクション
|
||||
*/
|
||||
export const initDb = async (force = false): Promise<Connection> => {
|
||||
// forceがtrueでない限り、既に接続が存在する場合はそれを返す
|
||||
if (!force) {
|
||||
try {
|
||||
const conn = getConnection();
|
||||
return Promise.resolve(conn);
|
||||
} catch (e) {
|
||||
// noop
|
||||
console.warn('connection is not found, so create');
|
||||
}
|
||||
}
|
||||
// forceがtrueでない限り、既に接続が存在する場合はそれを返す
|
||||
if (!force) {
|
||||
try {
|
||||
const conn = getConnection();
|
||||
return Promise.resolve(conn);
|
||||
} catch (e) {
|
||||
// noop
|
||||
console.warn('connection is not found, so create');
|
||||
}
|
||||
}
|
||||
|
||||
// 接続がないか、forceがtrueの場合は新規作成する
|
||||
return createConnection({
|
||||
type: 'postgres',
|
||||
host: config.db.host,
|
||||
port: config.db.port,
|
||||
username: config.db.user,
|
||||
password: config.db.pass,
|
||||
database: config.db.db,
|
||||
extra: config.db.extra,
|
||||
entities,
|
||||
});
|
||||
// 接続がないか、forceがtrueの場合は新規作成する
|
||||
return createConnection({
|
||||
type: 'postgres',
|
||||
host: config.db.host,
|
||||
port: config.db.port,
|
||||
username: config.db.user,
|
||||
password: config.db.pass,
|
||||
database: config.db.db,
|
||||
extra: config.db.extra,
|
||||
entities,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import axios from 'axios';
|
||||
import {printLog} from '../store';
|
||||
import {delay} from '../utils/delay';
|
||||
import {printLog} from '../store.js';
|
||||
import {delay} from '../utils/delay.js';
|
||||
|
||||
export const ua = `Mozilla/5.0 MisskeyTools +https://github.com/shrimpia/misskey-tools Node/${process.version}`;
|
||||
|
||||
|
@ -10,26 +10,26 @@ const RETRY_COUNT = 5;
|
|||
* Misskey APIを呼び出す
|
||||
*/
|
||||
export const api = async <T extends Record<string, unknown> = Record<string, unknown>>(host: string, endpoint: string, arg: Record<string, unknown>, token?: string): Promise<T> => {
|
||||
const a = { ...arg };
|
||||
if (token) {
|
||||
a.i = token;
|
||||
}
|
||||
const a = { ...arg };
|
||||
if (token) {
|
||||
a.i = token;
|
||||
}
|
||||
|
||||
for (let i = 0; i < RETRY_COUNT; i++) {
|
||||
let data: T;
|
||||
try {
|
||||
data = await axios.post<T>(`https://${host}/api/${endpoint}`, a).then(res => res.data);
|
||||
} catch (e) {
|
||||
printLog(`接続エラー: ${host}/api/${endpoint} リトライ中 (${i + 1} / ${RETRY_COUNT})\n${e}`, 'error');
|
||||
await delay(3000);
|
||||
continue;
|
||||
}
|
||||
if (!('error' in data)) {
|
||||
return data;
|
||||
}
|
||||
throw new MisskeyError((data as any).error);
|
||||
}
|
||||
throw new TimedOutError();
|
||||
for (let i = 0; i < RETRY_COUNT; i++) {
|
||||
let data: T;
|
||||
try {
|
||||
data = await axios.post<T>(`https://${host}/api/${endpoint}`, a).then(res => res.data);
|
||||
} catch (e) {
|
||||
printLog(`接続エラー: ${host}/api/${endpoint} リトライ中 (${i + 1} / ${RETRY_COUNT})\n${e}`, 'error');
|
||||
await delay(3000);
|
||||
continue;
|
||||
}
|
||||
if (!(typeof data === 'object' && 'error' in data)) {
|
||||
return data;
|
||||
}
|
||||
throw new MisskeyError((data as any).error);
|
||||
}
|
||||
throw new TimedOutError();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -39,25 +39,25 @@ export const api = async <T extends Record<string, unknown> = Record<string, unk
|
|||
* @returns トークンが有効ならtrue、無効ならfalse
|
||||
*/
|
||||
export const apiAvailable = async (host: string, i: string): Promise<boolean> => {
|
||||
try {
|
||||
const res = await api(host, 'i', {}, i);
|
||||
return !res.error;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const res = await api(host, 'i', {}, i);
|
||||
return !res.error;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export class TimedOutError extends Error {}
|
||||
|
||||
export class MisskeyError extends Error {
|
||||
constructor(public error: MisskeyErrorObject) {
|
||||
super();
|
||||
}
|
||||
constructor(public error: MisskeyErrorObject) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
export interface MisskeyErrorObject {
|
||||
message: string;
|
||||
code: string;
|
||||
id: string;
|
||||
kind: string;
|
||||
message: string;
|
||||
code: string;
|
||||
id: string;
|
||||
kind: string;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { User } from '../models/entities/user';
|
||||
import { api } from './misskey';
|
||||
import {format} from '../../common/functions/format';
|
||||
import {getScores} from '../functions/get-scores';
|
||||
import { User } from '../models/entities/user.js';
|
||||
import { api } from './misskey.js';
|
||||
import {format} from '../../common/functions/format.js';
|
||||
import {getScores} from '../functions/get-scores.js';
|
||||
|
||||
|
||||
/**
|
||||
|
@ -9,21 +9,21 @@ import {getScores} from '../functions/get-scores';
|
|||
* @param user ユーザー
|
||||
*/
|
||||
export const sendAlert = async (user: User) => {
|
||||
const text = format(user, await getScores(user));
|
||||
switch (user.alertMode) {
|
||||
case 'note':
|
||||
await sendNoteAlert(text, user);
|
||||
break;
|
||||
case 'notification':
|
||||
await sendNotificationAlert(text, user);
|
||||
break;
|
||||
case 'both':
|
||||
await Promise.all([
|
||||
sendNotificationAlert(text, user),
|
||||
sendNoteAlert(text, user),
|
||||
]);
|
||||
break;
|
||||
}
|
||||
const text = format(user, await getScores(user));
|
||||
switch (user.alertMode) {
|
||||
case 'note':
|
||||
await sendNoteAlert(text, user);
|
||||
break;
|
||||
case 'notification':
|
||||
await sendNotificationAlert(text, user);
|
||||
break;
|
||||
case 'both':
|
||||
await Promise.all([
|
||||
sendNotificationAlert(text, user),
|
||||
sendNoteAlert(text, user),
|
||||
]);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -32,16 +32,16 @@ export const sendAlert = async (user: User) => {
|
|||
* @param user ユーザー
|
||||
*/
|
||||
export const sendNoteAlert = async (text: string, user: User) => {
|
||||
const res = await api<Record<string, unknown>>(user.host, 'notes/create', {
|
||||
text,
|
||||
visibility: user.visibility,
|
||||
localOnly: user.localOnly,
|
||||
remoteFollowersOnly: user.remoteFollowersOnly,
|
||||
}, user.token);
|
||||
const res = await api<Record<string, unknown>>(user.host, 'notes/create', {
|
||||
text,
|
||||
visibility: user.visibility,
|
||||
localOnly: user.localOnly,
|
||||
remoteFollowersOnly: user.remoteFollowersOnly,
|
||||
}, user.token);
|
||||
|
||||
if (res.error) {
|
||||
throw res.error || res;
|
||||
}
|
||||
if (res.error) {
|
||||
throw res.error || res;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -50,13 +50,13 @@ export const sendNoteAlert = async (text: string, user: User) => {
|
|||
* @param user ユーザー
|
||||
*/
|
||||
export const sendNotificationAlert = async (text: string, user: User) => {
|
||||
const res = await api(user.host, 'notifications/create', {
|
||||
header: 'Misskey Tools',
|
||||
icon: 'https://i.imgur.com/B991yTl.png',
|
||||
body: text,
|
||||
}, user.token);
|
||||
const res = await api(user.host, 'notifications/create', {
|
||||
header: 'Misskey Tools',
|
||||
icon: 'https://i.imgur.com/B991yTl.png',
|
||||
body: text,
|
||||
}, user.token);
|
||||
|
||||
if (res.error) {
|
||||
throw res.error || res;
|
||||
}
|
||||
if (res.error) {
|
||||
throw res.error || res;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import cron from 'node-cron';
|
||||
import { deleteUser } from '../functions/users';
|
||||
import { MiUser, updateScore } from '../functions/update-score';
|
||||
import { updateRating } from '../functions/update-rating';
|
||||
import { Users } from '../models';
|
||||
import {sendNoteAlert, sendNotificationAlert} from './send-alert';
|
||||
import {api, MisskeyError, TimedOutError} from './misskey';
|
||||
import * as Store from '../store';
|
||||
import { User } from '../models/entities/user';
|
||||
import {groupBy} from '../utils/group-by';
|
||||
import {clearLog, printLog} from '../store';
|
||||
import {errorToString} from '../functions/error-to-string';
|
||||
import {Acct, toAcct} from '../models/acct';
|
||||
import {Count} from '../models/count';
|
||||
import {format} from '../../common/functions/format';
|
||||
import { deleteUser } from '../functions/users.js';
|
||||
import { MiUser, updateScore } from '../functions/update-score.js';
|
||||
import { updateRating } from '../functions/update-rating.js';
|
||||
import { Users } from '../models/index.js';
|
||||
import {sendNoteAlert, sendNotificationAlert} from './send-alert.js';
|
||||
import {api, MisskeyError, TimedOutError} from './misskey.js';
|
||||
import * as Store from '../store.js';
|
||||
import { User } from '../models/entities/user.js';
|
||||
import {groupBy} from '../utils/group-by.js';
|
||||
import {clearLog, printLog} from '../store.js';
|
||||
import {errorToString} from '../functions/error-to-string.js';
|
||||
import {Acct, toAcct} from '../models/acct.js';
|
||||
import {Count} from '../models/count.js';
|
||||
import {format} from '../../common/functions/format.js';
|
||||
import {delay} from '../utils/delay.js';
|
||||
|
||||
const ERROR_CODES_USER_REMOVED = ['NO_SUCH_USER', 'AUTHENTICATION_FAILED', 'YOUR_ACCOUNT_SUSPENDED'];
|
||||
|
||||
|
@ -20,106 +21,109 @@ const ERROR_CODES_USER_REMOVED = ['NO_SUCH_USER', 'AUTHENTICATION_FAILED', 'YOUR
|
|||
const userScoreCache = new Map<Acct, Count>();
|
||||
|
||||
export default (): void => {
|
||||
cron.schedule('0 0 0 * * *', work);
|
||||
cron.schedule('0 0 0 * * *', work);
|
||||
};
|
||||
|
||||
export const work = async () => {
|
||||
Store.dispatch({ nowCalculating: true });
|
||||
Store.dispatch({ nowCalculating: true });
|
||||
|
||||
clearLog();
|
||||
printLog('Started.');
|
||||
clearLog();
|
||||
printLog('Started.');
|
||||
|
||||
try {
|
||||
const users = await Users.find();
|
||||
const groupedUsers = groupBy(users, u => u.host);
|
||||
try {
|
||||
const users = await Users.find();
|
||||
const groupedUsers = groupBy(users, u => u.host);
|
||||
|
||||
printLog(`${users.length} アカウントのレート計算を開始します。`);
|
||||
await calculateAllRating(groupedUsers);
|
||||
Store.dispatch({ nowCalculating: false });
|
||||
printLog(`${users.length} アカウントのレート計算を開始します。`);
|
||||
await calculateAllRating(groupedUsers);
|
||||
Store.dispatch({ nowCalculating: false });
|
||||
|
||||
printLog(`${users.length} アカウントのアラート送信を開始します。`);
|
||||
await sendAllAlerts(groupedUsers);
|
||||
printLog(`${users.length} アカウントのアラート送信を開始します。`);
|
||||
await sendAllAlerts(groupedUsers);
|
||||
|
||||
printLog('ミス廃アラートワーカーは正常に完了しました。');
|
||||
} catch (e) {
|
||||
printLog('ミス廃アラートワーカーが異常終了しました。', 'error');
|
||||
printLog(e instanceof Error ? errorToString(e) : e, 'error');
|
||||
} finally {
|
||||
Store.dispatch({ nowCalculating: false });
|
||||
}
|
||||
printLog('ミス廃アラートワーカーは正常に完了しました。');
|
||||
} catch (e) {
|
||||
printLog('ミス廃アラートワーカーが異常終了しました。', 'error');
|
||||
printLog(e instanceof Error ? errorToString(e) : JSON.stringify(e, null, ' '), 'error');
|
||||
} finally {
|
||||
Store.dispatch({ nowCalculating: false });
|
||||
}
|
||||
};
|
||||
|
||||
const calculateAllRating = async (groupedUsers: [string, User[]][]) => {
|
||||
return await Promise.all(groupedUsers.map(kv => calculateRating(...kv)));
|
||||
return await Promise.all(groupedUsers.map(kv => calculateRating(...kv)));
|
||||
};
|
||||
|
||||
const calculateRating = async (host: string, users: User[]) => {
|
||||
for (const user of users) {
|
||||
let miUser: MiUser;
|
||||
try {
|
||||
miUser = await api<MiUser>(user.host, 'i', {}, user.token);
|
||||
} catch (e) {
|
||||
if (!(e instanceof Error)) {
|
||||
printLog('バグ:エラーオブジェクトはErrorを継承していないといけない', 'error');
|
||||
} else if (e instanceof MisskeyError) {
|
||||
if (ERROR_CODES_USER_REMOVED.includes(e.error.code)) {
|
||||
// ユーザーが削除されている場合、レコードからも消してとりやめ
|
||||
printLog(`アカウント ${toAcct(user)} は削除されているか、凍結されているか、トークンを失効しています。そのため、本システムからアカウントを削除します。`, 'warn');
|
||||
await deleteUser(user.username, user.host);
|
||||
} else {
|
||||
printLog(`Misskey エラー: ${JSON.stringify(e.error)}`, 'error');
|
||||
}
|
||||
} else if (e instanceof TimedOutError) {
|
||||
printLog(`サーバー ${user.host} との接続に失敗したため、このサーバーのレート計算を中断します。`, 'error');
|
||||
return;
|
||||
} else {
|
||||
// おそらく通信エラー
|
||||
printLog(`不明なエラーが発生しました。\n${errorToString(e)}`, 'error');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
userScoreCache.set(toAcct(user), miUser);
|
||||
for (const user of users) {
|
||||
let miUser: MiUser;
|
||||
try {
|
||||
miUser = await api<MiUser>(user.host, 'i', {}, user.token);
|
||||
} catch (e) {
|
||||
if (!(e instanceof Error)) {
|
||||
printLog('バグ:エラーオブジェクトはErrorを継承していないといけない', 'error');
|
||||
} else if (e instanceof MisskeyError) {
|
||||
if (ERROR_CODES_USER_REMOVED.includes(e.error.code)) {
|
||||
// ユーザーが削除されている場合、レコードからも消してとりやめ
|
||||
printLog(`アカウント ${toAcct(user)} は削除されているか、凍結されているか、トークンを失効しています。そのため、本システムからアカウントを削除します。`, 'warn');
|
||||
await deleteUser(user.username, user.host);
|
||||
} else {
|
||||
printLog(`Misskey エラー: ${JSON.stringify(e.error)}`, 'error');
|
||||
}
|
||||
} else if (e instanceof TimedOutError) {
|
||||
printLog(`サーバー ${user.host} との接続に失敗したため、このサーバーのレート計算を中断します。`, 'error');
|
||||
return;
|
||||
} else {
|
||||
// おそらく通信エラー
|
||||
printLog(`不明なエラーが発生しました。\n${errorToString(e)}`, 'error');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
userScoreCache.set(toAcct(user), miUser);
|
||||
|
||||
await updateRating(user, miUser);
|
||||
}
|
||||
printLog(`${host} ユーザー(${users.length}人) のレート計算が完了しました。`);
|
||||
await updateRating(user, miUser);
|
||||
}
|
||||
printLog(`${host} ユーザー(${users.length}人) のレート計算が完了しました。`);
|
||||
};
|
||||
|
||||
const sendAllAlerts = async (groupedUsers: [string, User[]][]) => {
|
||||
return await Promise.all(groupedUsers.map(kv => sendAlerts(...kv)));
|
||||
return await Promise.all(groupedUsers.map(kv => sendAlerts(...kv)));
|
||||
};
|
||||
|
||||
const sendAlerts = async (host: string, users: User[]) => {
|
||||
const models = users
|
||||
.map(user => {
|
||||
const count = userScoreCache.get(toAcct(user));
|
||||
if (count == null) return null;
|
||||
return {
|
||||
user,
|
||||
count,
|
||||
message: format(user, count),
|
||||
};
|
||||
})
|
||||
.filter(u => u != null) as {user: User, count: Count, message: string}[];
|
||||
const models = users
|
||||
.map(user => {
|
||||
const count = userScoreCache.get(toAcct(user));
|
||||
if (count == null) return null;
|
||||
return {
|
||||
user,
|
||||
count,
|
||||
message: format(user, count),
|
||||
};
|
||||
})
|
||||
.filter(u => u != null) as {user: User, count: Count, message: string}[];
|
||||
|
||||
// 何もしない
|
||||
for (const {user, count} of models.filter(m => m.user.alertMode === 'nothing')) {
|
||||
await updateScore(user, count);
|
||||
}
|
||||
// 何もしない
|
||||
for (const {user, count} of models.filter(m => m.user.alertMode === 'nothing')) {
|
||||
await updateScore(user, count);
|
||||
}
|
||||
|
||||
// 通知
|
||||
for (const {user, count, message} of models.filter(m => m.user.alertMode === 'notification' || m.user.alertMode === 'both')) {
|
||||
await sendNotificationAlert(message, user);
|
||||
if (user.alertMode === 'notification') {
|
||||
await updateScore(user, count);
|
||||
}
|
||||
}
|
||||
// 通知
|
||||
for (const {user, count, message} of models.filter(m => m.user.alertMode === 'notification' || m.user.alertMode === 'both')) {
|
||||
await sendNotificationAlert(message, user);
|
||||
if (user.alertMode === 'notification') {
|
||||
await updateScore(user, count);
|
||||
}
|
||||
}
|
||||
|
||||
// 通知
|
||||
for (const {user, count, message} of models.filter(m => m.user.alertMode === 'note')) {
|
||||
await sendNoteAlert(message, user);
|
||||
await updateScore(user, count);
|
||||
}
|
||||
// アラート
|
||||
for (const {user, count, message} of models.filter(m => m.user.alertMode === 'note' || m.user.alertMode === 'both')) {
|
||||
await sendNoteAlert(message, user);
|
||||
await Promise.all([
|
||||
updateScore(user, count),
|
||||
delay(1000),
|
||||
]);
|
||||
}
|
||||
|
||||
printLog(`${host} ユーザー(${users.length}人) へのアラート送信が完了しました。`);
|
||||
printLog(`${host} ユーザー(${users.length}人) へのアラート送信が完了しました。`);
|
||||
};
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
// getStateを介してステートを取得し、dispatchによって更新する
|
||||
// stateを直接編集できないようになっている
|
||||
|
||||
import {Log} from '../common/types/log';
|
||||
import {Log} from '../common/types/log.js';
|
||||
|
||||
/**
|
||||
* 初期値
|
||||
*/
|
||||
const defaultState: State = {
|
||||
nowCalculating: false,
|
||||
misshaiWorkerLog: [],
|
||||
nowCalculating: false,
|
||||
misshaiWorkerLog: [],
|
||||
};
|
||||
|
||||
let _state: Readonly<State> = defaultState;
|
||||
|
@ -18,8 +18,8 @@ let _state: Readonly<State> = defaultState;
|
|||
* ステートの型
|
||||
*/
|
||||
export type State = {
|
||||
nowCalculating: boolean,
|
||||
misshaiWorkerLog: Log[],
|
||||
nowCalculating: boolean,
|
||||
misshaiWorkerLog: Log[],
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -33,19 +33,20 @@ export const getState = () => Object.freeze({ ..._state });
|
|||
* @param mutation ステートの一部を更新するためのオブジェクト
|
||||
*/
|
||||
export const dispatch = (mutation: Partial<State>) => {
|
||||
_state = {
|
||||
..._state,
|
||||
...mutation,
|
||||
};
|
||||
_state = {
|
||||
..._state,
|
||||
...mutation,
|
||||
};
|
||||
};
|
||||
|
||||
export const clearLog = () => {
|
||||
dispatch({ misshaiWorkerLog: [] });
|
||||
dispatch({ misshaiWorkerLog: [] });
|
||||
};
|
||||
|
||||
export const printLog = (log: unknown, level: Log['level'] = 'info') => {
|
||||
dispatch({ misshaiWorkerLog: [
|
||||
...getState().misshaiWorkerLog,
|
||||
{ text: String(log), level, timestamp: new Date() },
|
||||
] });
|
||||
dispatch({ misshaiWorkerLog: [
|
||||
...getState().misshaiWorkerLog,
|
||||
{ text: String(log), level, timestamp: new Date() },
|
||||
] });
|
||||
console[level](log);
|
||||
};
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
type GetKeyFunction<K extends PropertyKey, V> = (cur: V, idx: number, src: readonly V[]) => K;
|
||||
|
||||
export const groupBy = <K extends PropertyKey, V>(array: readonly V[], getKey: GetKeyFunction<K, V>) => {
|
||||
return Array.from(
|
||||
array.reduce((map, cur, idx, src) => {
|
||||
const key = getKey(cur, idx, src);
|
||||
const list = map.get(key);
|
||||
if (list) list.push(cur);
|
||||
else map.set(key, [cur]);
|
||||
return map;
|
||||
}, new Map<K, V[]>())
|
||||
);
|
||||
return Array.from(
|
||||
array.reduce((map, cur, idx, src) => {
|
||||
const key = getKey(cur, idx, src);
|
||||
const list = map.get(key);
|
||||
if (list) list.push(cur);
|
||||
else map.set(key, [cur]);
|
||||
return map;
|
||||
}, new Map<K, V[]>())
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue