summaryrefslogtreecommitdiff
path: root/src/misc
diff options
context:
space:
mode:
Diffstat (limited to 'src/misc')
-rw-r--r--src/misc/acct/parse.ts4
-rw-r--r--src/misc/acct/render.ts8
-rw-r--r--src/misc/cafy-id.ts33
-rw-r--r--src/misc/cli/progressbar.ts85
-rw-r--r--src/misc/dependencyInfo.ts32
-rw-r--r--src/misc/environmentInfo.ts17
-rw-r--r--src/misc/fa.ts54
-rw-r--r--src/misc/get-note-summary.ts50
-rw-r--r--src/misc/get-notification-summary.ts28
-rw-r--r--src/misc/get-reaction-emoji.ts14
-rw-r--r--src/misc/get-user-name.ts5
-rw-r--r--src/misc/get-user-summary.ts18
-rw-r--r--src/misc/i18n.ts71
-rw-r--r--src/misc/is-quote.ts5
-rw-r--r--src/misc/license.ts13
-rw-r--r--src/misc/logger.ts53
-rw-r--r--src/misc/machineInfo.ts15
17 files changed, 505 insertions, 0 deletions
diff --git a/src/misc/acct/parse.ts b/src/misc/acct/parse.ts
new file mode 100644
index 0000000000..0c00fccef6
--- /dev/null
+++ b/src/misc/acct/parse.ts
@@ -0,0 +1,4 @@
+export default (acct: string) => {
+ const splitted = acct.split('@', 2);
+ return { username: splitted[0], host: splitted[1] || null };
+};
diff --git a/src/misc/acct/render.ts b/src/misc/acct/render.ts
new file mode 100644
index 0000000000..92ee2010a6
--- /dev/null
+++ b/src/misc/acct/render.ts
@@ -0,0 +1,8 @@
+type UserLike = {
+ host: string;
+ username: string;
+};
+
+export default (user: UserLike) => {
+ return user.host === null ? user.username : `${user.username}@${user.host}`;
+};
diff --git a/src/misc/cafy-id.ts b/src/misc/cafy-id.ts
new file mode 100644
index 0000000000..f3e1f5251b
--- /dev/null
+++ b/src/misc/cafy-id.ts
@@ -0,0 +1,33 @@
+import * as mongo from 'mongodb';
+import { Context } from 'cafy';
+
+export const isAnId = (x: any) => mongo.ObjectID.isValid(x);
+export const isNotAnId = (x: any) => !isAnId(x);
+
+/**
+ * ID
+ */
+export default class ID extends Context<mongo.ObjectID> {
+ constructor() {
+ super();
+
+ this.transform = v => {
+ if (isAnId(v) && !mongo.ObjectID.prototype.isPrototypeOf(v)) {
+ return new mongo.ObjectID(v);
+ } else {
+ return v;
+ }
+ };
+
+ this.push(v => {
+ if (!mongo.ObjectID.prototype.isPrototypeOf(v) && isNotAnId(v)) {
+ return new Error('must-be-an-id');
+ }
+ return true;
+ });
+ }
+
+ public getType() {
+ return super.getType('string');
+ }
+}
diff --git a/src/misc/cli/progressbar.ts b/src/misc/cli/progressbar.ts
new file mode 100644
index 0000000000..72496fdedc
--- /dev/null
+++ b/src/misc/cli/progressbar.ts
@@ -0,0 +1,85 @@
+import { EventEmitter } from 'events';
+import * as readline from 'readline';
+import chalk from 'chalk';
+
+/**
+ * Progress bar
+ */
+export default class extends EventEmitter {
+ public max: number;
+ public value: number;
+ public text: string;
+ private indicator: number;
+
+ constructor(max: number, text: string = null) {
+ super();
+ this.max = max;
+ this.value = 0;
+ this.text = text;
+ this.indicator = 0;
+ this.draw();
+
+ const iclock = setInterval(() => {
+ this.indicator = (this.indicator + 1) % 4;
+ this.draw();
+ }, 200);
+
+ this.on('complete', () => {
+ clearInterval(iclock);
+ });
+ }
+
+ public increment(): void {
+ this.value++;
+ this.draw();
+
+ // Check if it is fulfilled
+ if (this.value === this.max) {
+ this.indicator = null;
+
+ cll();
+ process.stdout.write(`${this.render()} -> ${chalk.bold('Complete')}\n`);
+
+ this.emit('complete');
+ }
+ }
+
+ public draw(): void {
+ const str = this.render();
+ cll();
+ process.stdout.write(str);
+ }
+
+ private render(): string {
+ const width = 30;
+ const t = this.text ? `${this.text} ` : '';
+
+ const v = Math.floor((this.value / this.max) * width);
+ const vs = new Array(v + 1).join('*');
+
+ const p = width - v;
+ const ps = new Array(p + 1).join(' ');
+
+ const percentage = Math.floor((this.value / this.max) * 100);
+ const percentages = chalk.gray(`(${percentage} %)`);
+
+ let i: string;
+ switch (this.indicator) {
+ case 0: i = '-'; break;
+ case 1: i = '\\'; break;
+ case 2: i = '|'; break;
+ case 3: i = '/'; break;
+ case null: i = '+'; break;
+ }
+
+ return `${i} ${t}[${vs}${ps}] ${this.value} / ${this.max} ${percentages}`;
+ }
+}
+
+/**
+ * Clear current line
+ */
+function cll(): void {
+ readline.clearLine(process.stdout, 0); // Clear current text
+ readline.cursorTo(process.stdout, 0, null); // Move cursor to the head of line
+}
diff --git a/src/misc/dependencyInfo.ts b/src/misc/dependencyInfo.ts
new file mode 100644
index 0000000000..09d2828222
--- /dev/null
+++ b/src/misc/dependencyInfo.ts
@@ -0,0 +1,32 @@
+import Logger from './logger';
+import { execSync } from 'child_process';
+
+export default class {
+ private logger: Logger;
+
+ constructor() {
+ this.logger = new Logger('Deps');
+ }
+
+ public showAll(): void {
+ this.show('MongoDB', 'mongo --version', x => x.match(/^MongoDB shell version:? v(.*)\r?\n/));
+ this.show('Redis', 'redis-server --version', x => x.match(/v=([0-9\.]*)/));
+ }
+
+ public show(serviceName: string, command: string, transform: (x: string) => RegExpMatchArray): void {
+ try {
+ // ステータス0以外のときにexecSyncはstderrをコンソール上に出力してしまうので
+ // プロセスからのstderrをすべて無視するように stdio オプションをセット
+ const x = execSync(command, { stdio: ['pipe', 'pipe', 'ignore'] });
+ const ver = transform(x.toString());
+ if (ver != null) {
+ this.logger.succ(`${serviceName} ${ver[1]} found`);
+ } else {
+ this.logger.warn(`${serviceName} not found`);
+ this.logger.warn(`Regexp used for version check of ${serviceName} is probably messed up`);
+ }
+ } catch (e) {
+ this.logger.warn(`${serviceName} not found`);
+ }
+ }
+}
diff --git a/src/misc/environmentInfo.ts b/src/misc/environmentInfo.ts
new file mode 100644
index 0000000000..cee42ef9c0
--- /dev/null
+++ b/src/misc/environmentInfo.ts
@@ -0,0 +1,17 @@
+import Logger from './logger';
+import isRoot = require('is-root');
+
+export default class {
+ public static show(): void {
+ const env = process.env.NODE_ENV;
+ const logger = new Logger('Env');
+ logger.info(typeof env == 'undefined' ? 'NODE_ENV is not set' : `NODE_ENV: ${env}`);
+
+ if (env !== 'production') {
+ logger.warn('The environment is not in production mode');
+ logger.warn('Do not use for production purpose');
+ }
+
+ logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`);
+ }
+}
diff --git a/src/misc/fa.ts b/src/misc/fa.ts
new file mode 100644
index 0000000000..077bb51e6d
--- /dev/null
+++ b/src/misc/fa.ts
@@ -0,0 +1,54 @@
+/**
+ * Replace fontawesome symbols
+ */
+
+import * as fontawesome from '@fortawesome/fontawesome';
+import regular from '@fortawesome/fontawesome-free-regular';
+import solid from '@fortawesome/fontawesome-free-solid';
+import brands from '@fortawesome/fontawesome-free-brands';
+
+fontawesome.library.add(regular, solid, brands);
+
+export const pattern = /%fa:(.+?)%/g;
+
+export const replacement = (match: string, key: string) => {
+ const args = key.split(' ');
+ let prefix = 'fas';
+ const classes: string[] = [];
+ let transform = '';
+ let name;
+
+ args.forEach(arg => {
+ if (arg == 'R' || arg == 'S' || arg == 'B') {
+ prefix =
+ arg == 'R' ? 'far' :
+ arg == 'S' ? 'fas' :
+ arg == 'B' ? 'fab' :
+ '';
+ } else if (arg[0] == '.') {
+ classes.push('fa-' + arg.substr(1));
+ } else if (arg[0] == '-') {
+ transform = arg.substr(1).split('|').join(' ');
+ } else {
+ name = arg;
+ }
+ });
+
+ const icon = fontawesome.icon({ prefix, iconName: name } as fontawesome.IconLookup, {
+ classes: classes,
+ transform: fontawesome.parse.transform(transform)
+ });
+
+ if (icon) {
+ return `<i data-fa class="${name}">${icon.html[0]}</i>`;
+ } else {
+ console.warn(`'${name}' not found in fa`);
+ return '';
+ }
+};
+
+export default (src: string) => {
+ return src.replace(pattern, replacement);
+};
+
+export const fa = fontawesome;
diff --git a/src/misc/get-note-summary.ts b/src/misc/get-note-summary.ts
new file mode 100644
index 0000000000..ec7c74cf9f
--- /dev/null
+++ b/src/misc/get-note-summary.ts
@@ -0,0 +1,50 @@
+/**
+ * 投稿を表す文字列を取得します。
+ * @param {*} note (packされた)投稿
+ */
+const summarize = (note: any): string => {
+ if (note.deletedAt) {
+ return '(削除された投稿)';
+ }
+
+ if (note.isHidden) {
+ return '(非公開の投稿)';
+ }
+
+ let summary = '';
+
+ // 本文
+ summary += note.text ? note.text : '';
+
+ // メディアが添付されているとき
+ if (note.media.length != 0) {
+ summary += ` (${note.media.length}つのメディア)`;
+ }
+
+ // 投票が添付されているとき
+ if (note.poll) {
+ summary += ' (投票)';
+ }
+
+ // 返信のとき
+ if (note.replyId) {
+ if (note.reply) {
+ summary += ` RE: ${summarize(note.reply)}`;
+ } else {
+ summary += ' RE: ...';
+ }
+ }
+
+ // Renoteのとき
+ if (note.renoteId) {
+ if (note.renote) {
+ summary += ` RP: ${summarize(note.renote)}`;
+ } else {
+ summary += ' RP: ...';
+ }
+ }
+
+ return summary.trim();
+};
+
+export default summarize;
diff --git a/src/misc/get-notification-summary.ts b/src/misc/get-notification-summary.ts
new file mode 100644
index 0000000000..71d4973ce9
--- /dev/null
+++ b/src/misc/get-notification-summary.ts
@@ -0,0 +1,28 @@
+import getUserName from './get-user-name';
+import getNoteSummary from './get-note-summary';
+import getReactionEmoji from './get-reaction-emoji';
+
+/**
+ * 通知を表す文字列を取得します。
+ * @param notification 通知
+ */
+export default function(notification: any): string {
+ switch (notification.type) {
+ case 'follow':
+ return `${getUserName(notification.user)}にフォローされました`;
+ case 'mention':
+ return `言及されました:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}」`;
+ case 'reply':
+ return `返信されました:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}」`;
+ case 'renote':
+ return `Renoteされました:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}」`;
+ case 'quote':
+ return `引用されました:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}」`;
+ case 'reaction':
+ return `リアクションされました:\n${getUserName(notification.user)} <${getReactionEmoji(notification.reaction)}>「${getNoteSummary(notification.note)}」`;
+ case 'poll_vote':
+ return `投票されました:\n${getUserName(notification.user)}「${getNoteSummary(notification.note)}」`;
+ default:
+ return `<不明な通知タイプ: ${notification.type}>`;
+ }
+}
diff --git a/src/misc/get-reaction-emoji.ts b/src/misc/get-reaction-emoji.ts
new file mode 100644
index 0000000000..c661205379
--- /dev/null
+++ b/src/misc/get-reaction-emoji.ts
@@ -0,0 +1,14 @@
+export default function(reaction: string): string {
+ switch (reaction) {
+ case 'like': return '👍';
+ case 'love': return '❤️';
+ case 'laugh': return '😆';
+ case 'hmm': return '🤔';
+ case 'surprise': return '😮';
+ case 'congrats': return '🎉';
+ case 'angry': return '💢';
+ case 'confused': return '😥';
+ case 'pudding': return '🍮';
+ default: return '';
+ }
+}
diff --git a/src/misc/get-user-name.ts b/src/misc/get-user-name.ts
new file mode 100644
index 0000000000..acd5e6626d
--- /dev/null
+++ b/src/misc/get-user-name.ts
@@ -0,0 +1,5 @@
+import { IUser } from '../models/user';
+
+export default function(user: IUser): string {
+ return user.name || '名無し';
+}
diff --git a/src/misc/get-user-summary.ts b/src/misc/get-user-summary.ts
new file mode 100644
index 0000000000..09cf5ebadc
--- /dev/null
+++ b/src/misc/get-user-summary.ts
@@ -0,0 +1,18 @@
+import { IUser, isLocalUser } from '../models/user';
+import getAcct from './acct/render';
+import getUserName from './get-user-name';
+
+/**
+ * ユーザーを表す文字列を取得します。
+ * @param user ユーザー
+ */
+export default function(user: IUser): string {
+ let string = `${getUserName(user)} (@${getAcct(user)})\n` +
+ `${user.notesCount}投稿、${user.followingCount}フォロー、${user.followersCount}フォロワー\n`;
+
+ if (isLocalUser(user)) {
+ string += `場所: ${user.profile.location}、誕生日: ${user.profile.birthday}\n`;
+ }
+
+ return string + `「${user.description}」`;
+}
diff --git a/src/misc/i18n.ts b/src/misc/i18n.ts
new file mode 100644
index 0000000000..a07af3e939
--- /dev/null
+++ b/src/misc/i18n.ts
@@ -0,0 +1,71 @@
+/**
+ * Replace i18n texts
+ */
+
+const locale = require('../../locales');
+
+export default class Replacer {
+ private lang: string;
+
+ public pattern = /%i18n:([a-z0-9_\-\.\/\|]+?)%/g;
+
+ constructor(lang: string) {
+ this.lang = lang;
+
+ this.get = this.get.bind(this);
+ this.replacement = this.replacement.bind(this);
+ }
+
+ public get(path: string, key: string): string {
+ if (!(this.lang in locale)) {
+ console.warn(`lang '${this.lang}' is not supported`);
+ return key; // Fallback
+ }
+
+ const texts = locale[this.lang];
+
+ let text = texts;
+
+ if (path) {
+ if (text.hasOwnProperty(path)) {
+ text = text[path];
+ } else {
+ if (this.lang === 'ja') console.warn(`path '${path}' not found`);
+ return key; // Fallback
+ }
+ }
+
+ // Check the key existance
+ const error = key.split('.').some(k => {
+ if (text.hasOwnProperty(k)) {
+ text = text[k];
+ return false;
+ } else {
+ return true;
+ }
+ });
+
+ if (error) {
+ if (this.lang === 'ja') console.warn(`key '${key}' not found in '${path}'`);
+ return key; // Fallback
+ } else if (typeof text !== 'string') {
+ if (this.lang === 'ja') console.warn(`key '${key}' is not string in '${path}'`);
+ return key; // Fallback
+ } else {
+ return text;
+ }
+ }
+
+ public replacement(match: string, key: string) {
+ let path = null;
+
+ if (key.indexOf('|') != -1) {
+ path = key.split('|')[0];
+ key = key.split('|')[1];
+ }
+
+ const txt = this.get(path, key);
+
+ return txt.replace(/'/g, '\\x27').replace(/"/g, '\\x22');
+ }
+}
diff --git a/src/misc/is-quote.ts b/src/misc/is-quote.ts
new file mode 100644
index 0000000000..420f03a489
--- /dev/null
+++ b/src/misc/is-quote.ts
@@ -0,0 +1,5 @@
+import { INote } from '../models/note';
+
+export default function(note: INote): boolean {
+ return note.renoteId != null && (note.text != null || note.poll != null || (note.mediaIds != null && note.mediaIds.length > 0));
+}
diff --git a/src/misc/license.ts b/src/misc/license.ts
new file mode 100644
index 0000000000..d36af665cd
--- /dev/null
+++ b/src/misc/license.ts
@@ -0,0 +1,13 @@
+import * as fs from 'fs';
+
+const license = fs.readFileSync(__dirname + '/../../LICENSE', 'utf-8');
+
+const licenseHtml = license
+ .replace(/\r\n/g, '\n')
+ .replace(/(.)\n(.)/g, '$1 $2')
+ .replace(/(^|\n)(.*?)($|\n)/g, '<p>$2</p>');
+
+export {
+ license,
+ licenseHtml
+};
diff --git a/src/misc/logger.ts b/src/misc/logger.ts
new file mode 100644
index 0000000000..04385251e4
--- /dev/null
+++ b/src/misc/logger.ts
@@ -0,0 +1,53 @@
+import chalk from 'chalk';
+import * as dateformat from 'dateformat';
+
+export default class Logger {
+ private domain: string;
+
+ constructor(domain: string) {
+ this.domain = domain;
+ }
+
+ public static log(level: string, message: string): void {
+ const time = dateformat(new Date(), 'HH:MM:ss');
+ console.log(`[${time} ${level}] ${message}`);
+ }
+
+ public static error(message: string): void {
+ (new Logger('')).error(message);
+ }
+
+ public static warn(message: string): void {
+ (new Logger('')).warn(message);
+ }
+
+ public static succ(message: string): void {
+ (new Logger('')).succ(message);
+ }
+
+ public static info(message: string): void {
+ (new Logger('')).info(message);
+ }
+
+ public log(level: string, message: string) {
+ const domain = this.domain.length > 0 ? `[${this.domain}] ` : '';
+ Logger.log(level, `${domain}${message}`);
+ }
+
+ public error(message: string): void { // 実行を継続できない状況で使う
+ this.log(chalk.red.bold('ERROR'), chalk.red.bold(message));
+ }
+
+ public warn(message: string): void { // 実行を継続できるが改善すべき状況で使う
+ this.log(chalk.yellow.bold('WARN'), chalk.yellow.bold(message));
+ }
+
+ public succ(message: string): void { // 何かに成功した状況で使う
+ this.log(chalk.blue.bold('INFO'), chalk.green.bold(message));
+ }
+
+ public info(message: string): void { // それ以外
+ this.log(chalk.blue.bold('INFO'), message);
+ }
+
+}
diff --git a/src/misc/machineInfo.ts b/src/misc/machineInfo.ts
new file mode 100644
index 0000000000..6049bcfc9c
--- /dev/null
+++ b/src/misc/machineInfo.ts
@@ -0,0 +1,15 @@
+import * as os from 'os';
+import Logger from './logger';
+
+export default class {
+ public static show(): void {
+ const totalmem = (os.totalmem() / 1024 / 1024 / 1024).toFixed(1);
+ const freemem = (os.freemem() / 1024 / 1024 / 1024).toFixed(1);
+ const logger = new Logger('Machine');
+ logger.info(`Hostname: ${os.hostname()}`);
+ logger.info(`Platform: ${process.platform}`);
+ logger.info(`Architecture: ${process.arch}`);
+ logger.info(`CPU: ${os.cpus().length} core`);
+ logger.info(`MEM: ${totalmem}GB (available: ${freemem}GB)`);
+ }
+}