This commit is contained in:
syuilo 2019-05-13 02:09:56 +09:00
parent 3d7e72f1ba
commit 2c74b27097
No known key found for this signature in database
GPG key ID: BDC4C49D06AB9D69
7 changed files with 322 additions and 0 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@ config.json
built built
node_modules node_modules
memory.json memory.json
font.ttf

View file

@ -21,6 +21,9 @@ Misskey用の日本語Botです。
``` ```
`npm install` して `npm run build` して `npm start` すれば起動できます `npm install` して `npm run build` して `npm start` すれば起動できます
## フォント
一部の機能にはフォントが必要です。藍にはフォントは同梱されていないので、ご自身でフォントをインストールディレクトリに`font.ttf`という名前で設置してください。
## 記憶 ## 記憶
藍は記憶の保持にインメモリデータベースを使用しており、藍のインストールディレクトリに `memory.json` という名前で永続化されます。 藍は記憶の保持にインメモリデータベースを使用しており、藍のインストールディレクトリに `memory.json` という名前で永続化されます。

View file

@ -20,6 +20,7 @@ import ServerModule from './modules/server';
import FollowModule from './modules/follow'; import FollowModule from './modules/follow';
import ValentineModule from './modules/valentine'; import ValentineModule from './modules/valentine';
import MazeModule from './modules/maze'; import MazeModule from './modules/maze';
import ChartModule from './modules/chart';
import chalk from 'chalk'; import chalk from 'chalk';
import * as request from 'request-promise-native'; import * as request from 'request-promise-native';
@ -71,6 +72,7 @@ promiseRetry(retry => {
new ValentineModule(), new ValentineModule(),
new KeywordModule(), new KeywordModule(),
new MazeModule(), new MazeModule(),
new ChartModule(),
]); ]);
}).catch(e => { }).catch(e => {
log(chalk.red('Failed to fetch the account')); log(chalk.red('Failed to fetch the account'));

107
src/modules/chart/index.ts Normal file
View file

@ -0,0 +1,107 @@
import autobind from 'autobind-decorator';
import Module from '../../module';
import serifs from '../../serifs';
import Message from '../../message';
import { renderChart } from './render-chart';
export default class extends Module {
public readonly name = 'chart';
@autobind
public install() {
this.post();
setInterval(this.post, 1000 * 60 * 3);
return {
mentionHook: this.mentionHook
};
}
@autobind
private async post() {
const now = new Date();
if (now.getHours() !== 23) return;
const date = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}`;
const data = this.getData();
if (data.lastPosted == date) return;
data.lastPosted = date;
this.setData(data);
this.log('Time to chart');
const file = await this.genChart('notes');
this.log('Posting...');
this.ai.post({
text: serifs.chart.post,
fileIds: [file.id]
});
}
@autobind
private async genChart(type, params?): Promise<any> {
this.log('Chart data fetching...');
let chart;
if (type === 'userNotes') {
const data = await this.ai.api('charts/user/notes', {
span: 'day',
limit: 30,
userId: params.userId
});
chart = {
datasets: [{
data: data.diffs.normal
}, {
data: data.diffs.reply
}, {
data: data.diffs.renote
}]
};
} else if (type === 'notes') {
const data = await this.ai.api('charts/notes', {
span: 'day',
limit: 30,
});
chart = {
datasets: [{
data: data.local.diffs.normal
}, {
data: data.local.diffs.reply
}, {
data: data.local.diffs.renote
}]
};
}
this.log('Chart rendering...');
const img = renderChart(chart);
this.log('Image uploading...');
const file = await this.ai.upload(img, {
filename: 'chart.png',
contentType: 'image/png'
});
return file;
}
@autobind
private async mentionHook(msg: Message) {
if (msg.includes(['チャート'])) {
this.log('Chart requested');
const file = await this.genChart('userNotes', {
userId: msg.userId
});
this.log('Replying...');
msg.replyWithFile(serifs.chart.foryou, file);
return {
reaction: 'like'
};
} else {
return false;
}
}
}

View file

@ -0,0 +1,201 @@
import { createCanvas, registerFont } from 'canvas';
const width = 1024 + 256;
const height = 512 + 256;
const margin = 128;
const chartAreaX = margin;
const chartAreaY = margin;
const chartAreaWidth = width - (margin * 2);
const chartAreaHeight = height - (margin * 2);
const lineWidth = 16;
const yAxisThickness = 2;
const colors = {
bg: '#434343',
text: '#e0e4cc',
yAxis: '#5a5a5a',
dataset: [
'#ff4e50',
'#c2f725',
'#69d2e7',
'#f38630',
'#f9d423',
]
};
const yAxisTicks = 4;
type Chart = {
title?: string;
datasets: {
title?: string;
data: number[];
}[];
};
export function renderChart(chart: Chart) {
registerFont('./font.ttf', { family: 'CustomFont' });
const canvas = createCanvas(width, height);
const ctx = canvas.getContext('2d');
ctx.antialias = 'default';
ctx.fillStyle = colors.bg;
ctx.beginPath();
ctx.fillRect(0, 0, width, height);
const xAxisCount = chart.datasets[0].data.length;
const serieses = chart.datasets.length;
let lowerBound = Infinity;
let upperBound = -Infinity;
for (let xAxis = 0; xAxis < xAxisCount; xAxis++) {
let v = 0;
for (let series = 0; series < serieses; series++) {
v += chart.datasets[series].data[xAxis];
}
if (v > upperBound) upperBound = v;
if (v < lowerBound) lowerBound = v;
}
// Calculate Y axis scale
const yAxisSteps = niceScale(lowerBound, upperBound, yAxisTicks);
const yAxisStepsMin = yAxisSteps[0];
const yAxisStepsMax = yAxisSteps[yAxisSteps.length - 1];
const yAxisRange = yAxisStepsMax - yAxisStepsMin;
// Draw Y axis
ctx.lineWidth = yAxisThickness;
ctx.lineCap = 'round';
ctx.strokeStyle = colors.yAxis;
for (let i = 0; i < yAxisSteps.length; i++) {
const step = yAxisSteps[yAxisSteps.length - i - 1];
const y = i * (chartAreaHeight / (yAxisSteps.length - 1));
ctx.beginPath();
ctx.lineTo(chartAreaX, chartAreaY + y);
ctx.lineTo(chartAreaX + chartAreaWidth, chartAreaY + y);
ctx.stroke();
ctx.font = '20px CustomFont';
ctx.fillStyle = colors.text;
ctx.fillText(step.toString(), chartAreaX, chartAreaY + y - 8);
}
const newDatasets = [];
for (let series = 0; series < serieses; series++) {
newDatasets.push({
data: []
});
}
for (let xAxis = 0; xAxis < xAxisCount; xAxis++) {
for (let series = 0; series < serieses; series++) {
newDatasets[series].data.push(chart.datasets[series].data[xAxis] / yAxisRange);
}
}
const perXAxisWidth = chartAreaWidth / xAxisCount;
let newUpperBound = -Infinity;
for (let xAxis = 0; xAxis < xAxisCount; xAxis++) {
let v = 0;
for (let series = 0; series < serieses; series++) {
v += newDatasets[series].data[xAxis];
}
if (v > newUpperBound) newUpperBound = v;
}
// Draw X axis
ctx.lineWidth = lineWidth;
ctx.lineCap = 'round';
for (let xAxis = 0; xAxis < xAxisCount; xAxis++) {
const xAxisPerTypeHeights = [];
for (let series = 0; series < serieses; series++) {
const v = newDatasets[series].data[xAxis];
const vHeight = (v / newUpperBound) * (chartAreaHeight - ((yAxisStepsMax - upperBound) / yAxisStepsMax * chartAreaHeight));
xAxisPerTypeHeights.push(vHeight);
}
for (let series = serieses - 1; series >= 0; series--) {
ctx.strokeStyle = colors.dataset[series % colors.dataset.length];
let total = 0;
for (let i = 0; i < series; i++) {
total += xAxisPerTypeHeights[i];
}
const height = xAxisPerTypeHeights[series];
const x = chartAreaX + (perXAxisWidth * ((xAxisCount - 1) - xAxis)) + (perXAxisWidth / 2);
const yTop = (chartAreaY + chartAreaHeight) - (total + height);
const yBottom = (chartAreaY + chartAreaHeight) - (total);
ctx.globalAlpha = 1 - (xAxis / xAxisCount);
ctx.beginPath();
ctx.lineTo(x, yTop);
ctx.lineTo(x, yBottom);
ctx.stroke();
}
}
return canvas.toBuffer();
}
// https://stackoverflow.com/questions/326679/choosing-an-attractive-linear-scale-for-a-graphs-y-axis
// https://github.com/apexcharts/apexcharts.js/blob/master/src/modules/Scales.js
// This routine creates the Y axis values for a graph.
function niceScale(lowerBound: number, upperBound: number, ticks: number): number[] {
// Calculate Min amd Max graphical labels and graph
// increments. The number of ticks defaults to
// 10 which is the SUGGESTED value. Any tick value
// entered is used as a suggested value which is
// adjusted to be a 'pretty' value.
//
// Output will be an array of the Y axis values that
// encompass the Y values.
const steps = [];
// Determine Range
const range = upperBound - lowerBound;
let tiks = ticks + 1;
// Adjust ticks if needed
if (tiks < 2) {
tiks = 2;
} else if (tiks > 2) {
tiks -= 2;
}
// Get raw step value
const tempStep = range / tiks;
// Calculate pretty step value
const mag = Math.floor(Math.log10(tempStep));
const magPow = Math.pow(10, mag);
const magMsd = (parseInt as any)(tempStep / magPow);
const stepSize = magMsd * magPow;
// build Y label array.
// Lower and upper bounds calculations
const lb = stepSize * Math.floor(lowerBound / stepSize);
const ub = stepSize * Math.ceil(upperBound / stepSize);
// Build array
let val = lb;
while (1) {
steps.push(val);
val += stepSize;
if (val > ub) {
break;
}
}
return steps;
}

View file

@ -316,6 +316,11 @@ export default {
post: '今日の迷路です! #AiMaze', post: '今日の迷路です! #AiMaze',
foryou: '描きました!' foryou: '描きました!'
}, },
chart: {
post: 'インスタンスの投稿数です!',
foryou: '描きました!'
},
}; };
export function getSerif(variant: string | string[]): string { export function getSerif(variant: string | string[]): string {

View file

@ -59,6 +59,9 @@ Misskeyにアカウントを作成して初めて投稿を行うと、藍がそ
### バレンタイン ### バレンタイン
藍がチョコレートをくれます。 藍がチョコレートをくれます。
### チャート
インスタンスの投稿チャートなどを投稿してくれます。
### サーバー監視 ### サーバー監視
サーバーの状態を監視し、負荷が高くなっているときは教えてくれます。 サーバーの状態を監視し、負荷が高くなっているときは教えてくれます。