resolve #10
This commit is contained in:
parent
ccd496722a
commit
d16f22feb5
7 changed files with 111 additions and 51 deletions
14
migration/1609941393782-template.ts
Normal file
14
migration/1609941393782-template.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import {MigrationInterface, QueryRunner} from 'typeorm';
|
||||||
|
|
||||||
|
export class template1609941393782 implements MigrationInterface {
|
||||||
|
name = 'template1609941393782'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query('ALTER TABLE "user" ADD "template" character varying(280)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query('ALTER TABLE "user" DROP COLUMN "template"');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,13 +1,66 @@
|
||||||
import { config } from '../config';
|
import { config } from '../config';
|
||||||
|
import { User } from '../models/entities/user';
|
||||||
import { Score } from '../types/Score';
|
import { Score } from '../types/Score';
|
||||||
|
|
||||||
export const format = (score: Score): string => `昨日のMisskeyの活動は
|
export const defaultTemplate = `昨日のMisskeyの活動は
|
||||||
|
|
||||||
ノート: ${score.notesCount}(${score.notesDelta})
|
ノート: {notesCount}({notesDelta})
|
||||||
フォロー : ${score.followingCount}(${score.followingDelta})
|
フォロー : {followingCount}({followingDelta})
|
||||||
フォロワー :${score.followersCount}(${score.followersDelta})
|
フォロワー :{followersCount}({followersDelta})
|
||||||
|
|
||||||
でした。
|
でした。
|
||||||
${config.url}
|
{url}`;
|
||||||
|
|
||||||
#misshaialert`;
|
export type Variable = {
|
||||||
|
description?: string;
|
||||||
|
replace?: string | ((score: Score, user: User) => string);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const variables: Record<string, Variable> = {
|
||||||
|
notesCount: {
|
||||||
|
description: 'ノート数',
|
||||||
|
replace: (score) => String(score.notesCount),
|
||||||
|
},
|
||||||
|
followingCount: {
|
||||||
|
description: 'フォロー数',
|
||||||
|
replace: (score) => String(score.followingCount),
|
||||||
|
},
|
||||||
|
followersCount: {
|
||||||
|
description: 'フォロワー数',
|
||||||
|
replace: (score) => String(score.followersCount),
|
||||||
|
},
|
||||||
|
notesDelta: {
|
||||||
|
description: '昨日とのノート数の差',
|
||||||
|
replace: (score) => String(score.notesDelta),
|
||||||
|
},
|
||||||
|
followingDelta: {
|
||||||
|
description: '昨日とのフォロー数の差',
|
||||||
|
replace: (score) => String(score.followingDelta),
|
||||||
|
},
|
||||||
|
followersDelta: {
|
||||||
|
description: '昨日とのフォロワー数の差',
|
||||||
|
replace: (score) => String(score.followersDelta),
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
description: 'みす廃アラートのURL',
|
||||||
|
replace: config.url,
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
description: 'ユーザー名',
|
||||||
|
replace: (_, user) => String(user.username),
|
||||||
|
},
|
||||||
|
host: {
|
||||||
|
description: '所属するインスタンスのホスト名',
|
||||||
|
replace: (_, user) => String(user.host),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const variableRegex = /\{([a-zA-Z0-9_]+?)\}/g;
|
||||||
|
|
||||||
|
export const format = (score: Score, user: User): string => {
|
||||||
|
const template = user.template || defaultTemplate;
|
||||||
|
return template.replace(variableRegex, (m, name) => {
|
||||||
|
const v = variables[name];
|
||||||
|
return !v || !v.replace ? m : typeof v.replace === 'function' ? v.replace(score, user) : v.replace;
|
||||||
|
}) + '\n\n#misshaialert';
|
||||||
|
};
|
||||||
|
|
|
@ -72,4 +72,11 @@ export class User {
|
||||||
default: false,
|
default: false,
|
||||||
})
|
})
|
||||||
public remoteFollowersOnly: boolean;
|
public remoteFollowersOnly: boolean;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'varchar',
|
||||||
|
length: 280,
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
public template: string | null;
|
||||||
}
|
}
|
|
@ -13,6 +13,7 @@ import { AlertMode, alertModes } from '../types/AlertMode';
|
||||||
import { Users } from '../models';
|
import { Users } from '../models';
|
||||||
import { send } from '../services/send';
|
import { send } from '../services/send';
|
||||||
import { visibilities, Visibility } from '../types/Visibility';
|
import { visibilities, Visibility } from '../types/Visibility';
|
||||||
|
import { defaultTemplate, variables } from '../functions/format';
|
||||||
|
|
||||||
export const router = new Router<DefaultState, Context>();
|
export const router = new Router<DefaultState, Context>();
|
||||||
|
|
||||||
|
@ -67,6 +68,8 @@ router.get('/', async ctx => {
|
||||||
user,
|
user,
|
||||||
// To Activate Groundpolis Mode
|
// To Activate Groundpolis Mode
|
||||||
isGroundpolis: meta.version.includes('gp'),
|
isGroundpolis: meta.version.includes('gp'),
|
||||||
|
defaultTemplate,
|
||||||
|
templateVariables: variables,
|
||||||
usersCount: await getUserCount(),
|
usersCount: await getUserCount(),
|
||||||
score: await getScores(user),
|
score: await getScores(user),
|
||||||
from: ctx.query.from,
|
from: ctx.query.from,
|
||||||
|
@ -209,6 +212,8 @@ router.post('/update-settings', async ctx => {
|
||||||
|
|
||||||
const flag = ctx.request.body.flag;
|
const flag = ctx.request.body.flag;
|
||||||
|
|
||||||
|
const template = ctx.request.body.template?.trim();
|
||||||
|
|
||||||
const token = ctx.cookies.get('token');
|
const token = ctx.cookies.get('token');
|
||||||
if (!token) {
|
if (!token) {
|
||||||
await die(ctx, 'ログインしていません');
|
await die(ctx, 'ログインしていません');
|
||||||
|
@ -226,6 +231,7 @@ router.post('/update-settings', async ctx => {
|
||||||
alertMode: mode,
|
alertMode: mode,
|
||||||
localOnly: flag === 'localOnly',
|
localOnly: flag === 'localOnly',
|
||||||
remoteFollowersOnly: flag === 'remoteFollowersOnly',
|
remoteFollowersOnly: flag === 'remoteFollowersOnly',
|
||||||
|
template: template === defaultTemplate || !template ? null : template,
|
||||||
visibility,
|
visibility,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { getScores } from '../functions/get-scores';
|
||||||
import { api } from './misskey';
|
import { api } from './misskey';
|
||||||
|
|
||||||
export const send = async (user: User): Promise<void> => {
|
export const send = async (user: User): Promise<void> => {
|
||||||
const text = format(await getScores(user));
|
const text = format(await getScores(user), user);
|
||||||
|
|
||||||
if (user.alertMode === 'note') {
|
if (user.alertMode === 'note') {
|
||||||
console.info(`send ${user.username}@${user.host}'s misshaialert as a note`);
|
console.info(`send ${user.username}@${user.host}'s misshaialert as a note`);
|
||||||
|
|
|
@ -70,77 +70,57 @@ block content
|
||||||
| スコア通知方法に「Misskey に通知」を選んでいる場合、Groundpolis v3 および Misskey v12 の最新版以外では動作しません。めいすきーや古いバージョンをお使いの方は、「自動的にノートを投稿」をお使いください。
|
| スコア通知方法に「Misskey に通知」を選んでいる場合、Groundpolis v3 および Misskey v12 の最新版以外では動作しません。めいすきーや古いバージョンをお使いの方は、「自動的にノートを投稿」をお使いください。
|
||||||
form(method="post", action="/update-settings")
|
form(method="post", action="/update-settings")
|
||||||
p: label スコア通知方法:
|
p: label スコア通知方法:
|
||||||
select#alertModeSelector(name="alertMode", tabindex=1)
|
select#alertModeSelector(name="alertMode")
|
||||||
each set in alertModes
|
each set in alertModes
|
||||||
option(value=set[0], selected=(user.alertMode === set[0]))= set[1]
|
option(value=set[0], selected=(user.alertMode === set[0]))= set[1]
|
||||||
#hideWhenAlertModeNotNote
|
#hideWhenAlertModeNotNote
|
||||||
p: label 公開範囲:
|
p: label 公開範囲:
|
||||||
select(name="visibility", tabindex=2)
|
select(name="visibility")
|
||||||
each set in visibilities
|
each set in visibilities
|
||||||
option(value=set[0], selected=(user.visibility === set[0]))= set[1]
|
option(value=set[0], selected=(user.visibility === set[0]))= set[1]
|
||||||
p
|
p
|
||||||
| フラグ <br />
|
| フラグ <br />
|
||||||
label
|
label
|
||||||
input(type="radio", name="flag", value="none", checked=!user.localOnly && !user.remoteFollowersOnly, tabindex=3)
|
input(type="radio", name="flag", value="none", checked=!user.localOnly && !user.remoteFollowersOnly)
|
||||||
| なし(標準)<br />
|
| なし(標準)<br />
|
||||||
label
|
label
|
||||||
input(type="radio", name="flag", value="localOnly", checked=user.localOnly, tabindex=4)
|
input(type="radio", name="flag", value="localOnly", checked=user.localOnly)
|
||||||
| ローカルのみ<br />
|
| ローカルのみ<br />
|
||||||
if isGroundpolis
|
if isGroundpolis
|
||||||
label
|
label
|
||||||
input(type="radio", name="flag", value="remoteFollowersOnly", checked=user.remoteFollowersOnly, tabindex=5)
|
input(type="radio", name="flag", value="remoteFollowersOnly", checked=user.remoteFollowersOnly)
|
||||||
| リモートフォロワーとローカル<br />
|
| リモートフォロワーとローカル<br />
|
||||||
#hideWhenAlertModeNothing
|
#hideWhenAlertModeNothing
|
||||||
div
|
div
|
||||||
label 投稿テンプレート
|
label 投稿テンプレート<br/>
|
||||||
div: textarea(name="template", disabled, tabindex=6)
|
textarea(name="template")=user.template || defaultTemplate
|
||||||
details(tabindex=7)
|
details()
|
||||||
summary ヘルプ
|
summary ヘルプ
|
||||||
p
|
ul
|
||||||
|
li 空欄にすると、デフォルト値にリセットされます。
|
||||||
|
li ハッシュタグ #misshaialert は、テンプレートに関わらず自動付与されます。
|
||||||
|
li
|
||||||
code {notesCount}
|
code {notesCount}
|
||||||
| などのテキストを挿入することで、投稿時に自動的に値が埋め込まれます。これを変数といいます。変数の表を次に示します。
|
| といった形式のテキストは変数として扱われ、これを含めると投稿時に自動的に値が埋め込まれます。
|
||||||
table
|
table
|
||||||
thead: tr
|
thead: tr
|
||||||
th 変数
|
th 変数
|
||||||
th 説明
|
th 説明
|
||||||
tbody
|
tbody
|
||||||
|
each val, key in templateVariables
|
||||||
tr
|
tr
|
||||||
td {notesCount}
|
td=key
|
||||||
td ノート数
|
td=val.description
|
||||||
tr
|
button.primary(type="submit") 保存
|
||||||
td {followingCount}
|
|
||||||
td フォロー数
|
|
||||||
tr
|
|
||||||
td {followersCount}
|
|
||||||
td フォロワー数
|
|
||||||
tr
|
|
||||||
td {notesDelta}
|
|
||||||
td 昨日とのノート数の差
|
|
||||||
tr
|
|
||||||
td {followingDelta}
|
|
||||||
td 昨日とのフォロー数の差
|
|
||||||
tr
|
|
||||||
td {followersDelta}
|
|
||||||
td 昨日とのフォロワー数の差
|
|
||||||
tr
|
|
||||||
td {url}
|
|
||||||
td みす廃アラートのURL
|
|
||||||
tr
|
|
||||||
td {ranking}
|
|
||||||
td みす廃ランキングの順位
|
|
||||||
tr
|
|
||||||
td {rating}
|
|
||||||
td みす廃レート
|
|
||||||
button.primary(type="submit", tabindex=8) 保存
|
|
||||||
|
|
||||||
|
|
||||||
section.xd-card#settings
|
section.xd-card#settings
|
||||||
.header
|
.header
|
||||||
h1.title 操作
|
h1.title 操作
|
||||||
.body
|
.body
|
||||||
form.mb-2(action="/send", method="post", tabindex=9): button#send(style="display: inline-block") アラートをテスト送信
|
form.mb-2(action="/send", method="post"): button#send(style="display: inline-block") アラートをテスト送信
|
||||||
form.mb-2(action="/logout", method="post", tabindex=10): button#logout(style="display: inline-block") ログアウト
|
form.mb-2(action="/logout", method="post"): button#logout(style="display: inline-block") ログアウト
|
||||||
form.mb-2(action="/optout", method="post", tabindex=11): button.danger#optout(style="display: inline-block") アカウント連携を解除する
|
form.mb-2(action="/optout", method="post"): button.danger#optout(style="display: inline-block") アカウント連携を解除する
|
||||||
|
|
||||||
block script
|
block script
|
||||||
script.
|
script.
|
||||||
|
|
|
@ -171,10 +171,10 @@ textarea {
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
height: 8rem;
|
height: 8rem;
|
||||||
line-height: 1;
|
line-height: 1.2;
|
||||||
|
color: var(--fg);
|
||||||
&:focus {
|
&:focus {
|
||||||
border: 1px solid var(--primary);
|
border: 1px solid var(--primary);
|
||||||
color: var(--fg);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue