Compare commits

...

41 Commits

Author SHA1 Message Date
syuilo
e97951fc51 10.95.0 2019-03-15 13:54:08 +09:00
syuilo
dfabdef60f Resolve #4501 2019-03-15 13:51:23 +09:00
syuilo
5a87763193 New Crowdin translations (#4481)
* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (Czech)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (English)
2019-03-15 13:48:46 +09:00
syuilo
6bb90f56fa ジョブを一覧できるように 2019-03-15 13:48:17 +09:00
syuilo
c883ae1350 🎨 2019-03-15 13:14:50 +09:00
syuilo
09e25e6a02 Better queue chart 2019-03-15 13:09:19 +09:00
syuilo
bf5d43054b Fix bug 2019-03-15 13:09:05 +09:00
MeiMei
63b3c65691 ファビコンが保存されないのを修正 (#4500)
* Fix ファビコン保存されない

* Fix meta
2019-03-15 12:40:10 +09:00
syuilo
f193da7f67 🎨 2019-03-15 02:17:10 +09:00
syuilo
40f38c2c0a Improve queue page 2019-03-15 01:56:40 +09:00
syuilo
db439ef804 🎨 2019-03-15 01:43:11 +09:00
MeiMei
56eb896a03 Accept Article object (#4499) 2019-03-15 00:23:24 +09:00
Acid Chicken (硫酸鶏)
68d43e43b6 Fix hashtag style 2019-03-15 00:03:24 +09:00
Acid Chicken (硫酸鶏)
c60517e49a Follow #3676 2019-03-14 22:18:31 +09:00
Acid Chicken (硫酸鶏)
3f59d261f2 Follow #3676 2019-03-14 22:18:10 +09:00
Acid Chicken (硫酸鶏)
4068d220e5 Follow #3676 2019-03-14 22:17:26 +09:00
syuilo
18968e7208 Fix bug 2019-03-14 21:51:33 +09:00
Acid Chicken (硫酸鶏)
38656103c0 Add angle bracket covered url syntax to mfm (#4483)
* Add angle bracket covered url syntax to mfm

* Fix path

* Fix match

* Fix index
2019-03-14 21:23:15 +09:00
syuilo
0f65b1bcc5 10.94.0 2019-03-14 16:40:06 +09:00
syuilo
a628821834 Improve readability 2019-03-14 16:35:07 +09:00
syuilo
6ceff60c1e Faviconを可変にするなど 2019-03-14 16:30:51 +09:00
syuilo
d762a6ce58 Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-14 15:19:09 +09:00
syuilo
75a8037a46 Fix #4489 2019-03-14 15:19:02 +09:00
MeiMei
1179920790 unFollowAll on suspend (#4490)
* unFollowAll on suspend

* use services

* silent
2019-03-14 15:16:07 +09:00
Acid Chicken (硫酸鶏)
b323a160e3 Follow #3676 2019-03-14 01:20:25 +09:00
syuilo
b157e9535e Merge branch 'develop' of https://github.com/syuilo/misskey into develop 2019-03-13 23:27:21 +09:00
syuilo
7668475bd6 🎨 2019-03-13 23:27:11 +09:00
syuilo
8bda2a1fb7 Refactor 2019-03-13 23:04:17 +09:00
Acid Chicken (硫酸鶏)
b092086b5b Add languages (#4452) 2019-03-13 14:22:55 +09:00
MeiMei
69a0d9034f Fix #4486 (#4487) 2019-03-13 11:21:16 +09:00
syuilo
f5be8fd313 10.93.1 2019-03-13 09:34:14 +09:00
syuilo
7f835d7f76 🎨 2019-03-13 09:32:10 +09:00
syuilo
ddbb7c5993 🎨 2019-03-13 09:26:38 +09:00
syuilo
00a3fe39e8 Update dependencies 🚀 2019-03-13 09:19:48 +09:00
syuilo
7537fb88d4 Refactor 2019-03-13 00:14:44 +09:00
syuilo
a81bc71a1e Resolve #4454 2019-03-13 00:13:56 +09:00
MeiMei
0a0aa0e2db Fix #4484 (#4485)
* Fix #4484

* import order
2019-03-12 23:38:11 +09:00
syuilo
c56b94ae96 Add type annotation to avoid type error 2019-03-12 23:31:18 +09:00
syuilo
e90712706d Add icons 🎨 2019-03-12 23:30:44 +09:00
syuilo
eb0623331f 🎨 2019-03-12 23:14:18 +09:00
MeiMei
d15bd59109 Fix queue charts (#4482) 2019-03-12 21:53:36 +09:00
65 changed files with 666 additions and 137 deletions

View File

@@ -5,6 +5,29 @@ If you encounter any problems with updating, please try the following:
1. `npm run clean` or `npm run cleanall` 1. `npm run clean` or `npm run cleanall`
2. Retry update (Don't forget `npm i`) 2. Retry update (Don't forget `npm i`)
10.95.0
----------
* ジョブを一覧できるように
* MFMでURLを明示する構文の追加
* Articleタイプのアクティビティを受け入れるように
* 凍結されたユーザーをサジェストしないように
* ファビコンが保存されないのを修正
* キューのジョブクリアの動作を修正
* デザインの調整
10.94.0
----------
* Faviconを設定できるように
* アカウントを凍結したときすべてのフォローを解除するように
* シェアページが機能していない問題を修正
* インスタンスブロックをしていてもRenote等すると取得されてしまう問題を修正
* デザインの調整
10.93.1
----------
* データのエクスポートとインポートの動作を修正
* デザインの調整
10.93.0 10.93.0
---------- ----------
* フォローリストをインポートできるように * フォローリストをインポートできるように

View File

@@ -304,6 +304,7 @@ common/views/pages/explore.vue:
explore: "Prozkoumat {host}" explore: "Prozkoumat {host}"
common/views/components/url-preview.vue: common/views/components/url-preview.vue:
enable-player: "Otevřít v přehrávači" enable-player: "Otevřít v přehrávači"
disable-player: "Zavřít přehrávač"
common/views/components/user-list.vue: common/views/components/user-list.vue:
no-users: "Žádní uživatelé" no-users: "Žádní uživatelé"
common/views/components/games/reversi/reversi.vue: common/views/components/games/reversi/reversi.vue:
@@ -848,8 +849,6 @@ admin/views/dashboard.vue:
instances: "Instance" instances: "Instance"
this-instance: "Tato instance" this-instance: "Tato instance"
federated: "Z fediversu" federated: "Z fediversu"
admin/views/queue.vue:
operation: "Akce"
admin/views/abuse.vue: admin/views/abuse.vue:
details: "Popis" details: "Popis"
remove-report: "Odstranit" remove-report: "Odstranit"

View File

@@ -169,9 +169,9 @@ common:
deck-column-align-flexible: "Flexible" deck-column-align-flexible: "Flexible"
deck-column-width: "Deck column width" deck-column-width: "Deck column width"
deck-column-width-narrow: "Narrow" deck-column-width-narrow: "Narrow"
deck-column-width-narrower: "Somewhat narrow" deck-column-width-narrower: "Narrower"
deck-column-width-normal: "Regular" deck-column-width-normal: "Regular"
deck-column-width-wider: "Somewhat wide" deck-column-width-wider: "Slightly wide"
deck-column-width-wide: "Wide" deck-column-width-wide: "Wide"
use-shadow: "Use shadows in the UI" use-shadow: "Use shadows in the UI"
rounded-corners: "Round the corners of the UI" rounded-corners: "Round the corners of the UI"
@@ -1057,7 +1057,7 @@ admin/views/dashboard.vue:
this-instance: "This instance" this-instance: "This instance"
federated: "Federated" federated: "Federated"
admin/views/queue.vue: admin/views/queue.vue:
operation: "Action(s)" title: "Queue"
remove-all-jobs: "Clear all queued jobs" remove-all-jobs: "Clear all queued jobs"
admin/views/abuse.vue: admin/views/abuse.vue:
title: "Abuse" title: "Abuse"

View File

@@ -927,7 +927,6 @@ admin/views/dashboard.vue:
this-instance: "Cette instance" this-instance: "Cette instance"
federated: "Fédérées" federated: "Fédérées"
admin/views/queue.vue: admin/views/queue.vue:
operation: "Action(s)"
remove-all-jobs: "Enlever toutes les tâches en attente" remove-all-jobs: "Enlever toutes les tâches en attente"
admin/views/abuse.vue: admin/views/abuse.vue:
title: "Abus" title: "Abus"

View File

@@ -14,6 +14,7 @@ const merge = (...args) => args.reduce((a, c) => ({
}), {}); }), {});
const languages = [ const languages = [
'cs-CZ',
'de-DE', 'de-DE',
'en-US', 'en-US',
'es-ES', 'es-ES',
@@ -24,9 +25,11 @@ const languages = [
'nl-NL', 'nl-NL',
'pl-PL', 'pl-PL',
'zh-CN', 'zh-CN',
'zh-TW',
]; ];
const primaries = { const primaries = {
'en': 'US',
'ja': 'JP', 'ja': 'JP',
'zh': 'CN', 'zh': 'CN',
}; };

View File

@@ -860,8 +860,6 @@ admin/views/dashboard.vue:
instances: "インスタンス" instances: "インスタンス"
this-instance: "ワイのインスタンス" this-instance: "ワイのインスタンス"
federated: "連合" federated: "連合"
admin/views/queue.vue:
operation: "操作"
admin/views/abuse.vue: admin/views/abuse.vue:
details: "もっと" details: "もっと"
remove-report: "削除" remove-report: "削除"

View File

@@ -314,6 +314,7 @@ common/views/pages/explore.vue:
users-info: "현재 {users} 사용자가 등록되어 있습니다" users-info: "현재 {users} 사용자가 등록되어 있습니다"
common/views/components/url-preview.vue: common/views/components/url-preview.vue:
enable-player: "플레이어 열기" enable-player: "플레이어 열기"
disable-player: "플레이어 닫기"
common/views/components/user-list.vue: common/views/components/user-list.vue:
no-users: "사용자가 없습니다" no-users: "사용자가 없습니다"
common/views/components/games/reversi/reversi.vue: common/views/components/games/reversi/reversi.vue:
@@ -1056,7 +1057,7 @@ admin/views/dashboard.vue:
this-instance: "이 인스턴스" this-instance: "이 인스턴스"
federated: "연합" federated: "연합"
admin/views/queue.vue: admin/views/queue.vue:
operation: "동작" title: ""
remove-all-jobs: "모든 작업 제거" remove-all-jobs: "모든 작업 제거"
admin/views/abuse.vue: admin/views/abuse.vue:
title: "스팸 신고" title: "스팸 신고"

View File

@@ -121,12 +121,15 @@ common:
other: "Inne" other: "Inne"
appearance: "Wygląd" appearance: "Wygląd"
behavior: "Zachowanie" behavior: "Zachowanie"
fetch-on-scroll: "Automatycznie ładuj po przeciągnięciu w dół"
note-visibility: "Widoczność wpisów" note-visibility: "Widoczność wpisów"
web-search-engine: "Wyszukiwarka internetowa"
line-width-thin: "Cienka" line-width-thin: "Cienka"
line-width-normal: "Normalna" line-width-normal: "Normalna"
line-width-thick: "Gruba" line-width-thick: "Gruba"
font-size: "Rozmiar tekstu" font-size: "Rozmiar tekstu"
font-size-medium: "Normalna" font-size-medium: "Normalna"
font-size-large: "Trochę duży"
font-size-x-large: "Duży" font-size-x-large: "Duży"
deck-column-align-center: "Po środku" deck-column-align-center: "Po środku"
deck-column-align-left: "Z lewej" deck-column-align-left: "Z lewej"
@@ -137,6 +140,8 @@ common:
deck-column-width-normal: "Normalna" deck-column-width-normal: "Normalna"
deck-column-width-wider: "Trochę szerokie" deck-column-width-wider: "Trochę szerokie"
deck-column-width-wide: "Szeroka" deck-column-width-wide: "Szeroka"
wallpaper: "Tapeta"
choose-wallpaper: "Wybierz tapetę"
timeline: "Oś czasu" timeline: "Oś czasu"
navbar-position-left: "Z lewej" navbar-position-left: "Z lewej"
search: "Szukaj" search: "Szukaj"

View File

@@ -314,6 +314,7 @@ common/views/pages/explore.vue:
users-info: "当前有{users}个注册用户" users-info: "当前有{users}个注册用户"
common/views/components/url-preview.vue: common/views/components/url-preview.vue:
enable-player: "打开播放器" enable-player: "打开播放器"
disable-player: "关闭播放器"
common/views/components/user-list.vue: common/views/components/user-list.vue:
no-users: "无用户" no-users: "无用户"
common/views/components/games/reversi/reversi.vue: common/views/components/games/reversi/reversi.vue:
@@ -1056,7 +1057,7 @@ admin/views/dashboard.vue:
this-instance: "此实例" this-instance: "此实例"
federated: "联合" federated: "联合"
admin/views/queue.vue: admin/views/queue.vue:
operation: "操作" title: "队列"
remove-all-jobs: "清除所有作业" remove-all-jobs: "清除所有作业"
admin/views/abuse.vue: admin/views/abuse.vue:
title: "举报垃圾信息" title: "举报垃圾信息"

View File

@@ -1,7 +1,7 @@
{ {
"name": "misskey", "name": "misskey",
"author": "syuilo <i@syuilo.com>", "author": "syuilo <i@syuilo.com>",
"version": "10.93.0", "version": "10.95.0",
"codename": "nighthike", "codename": "nighthike",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -46,7 +46,6 @@
"@types/gulp-uglify": "3.0.6", "@types/gulp-uglify": "3.0.6",
"@types/gulp-util": "3.0.34", "@types/gulp-util": "3.0.34",
"@types/is-root": "1.0.0", "@types/is-root": "1.0.0",
"@types/is-svg": "3.0.0",
"@types/is-url": "1.2.28", "@types/is-url": "1.2.28",
"@types/js-yaml": "3.12.0", "@types/js-yaml": "3.12.0",
"@types/jsdom": "12.2.3", "@types/jsdom": "12.2.3",
@@ -96,7 +95,7 @@
"@types/websocket": "0.0.40", "@types/websocket": "0.0.40",
"@types/ws": "6.0.1", "@types/ws": "6.0.1",
"animejs": "3.0.1", "animejs": "3.0.1",
"apexcharts": "3.5.0", "apexcharts": "3.6.2",
"autobind-decorator": "2.4.0", "autobind-decorator": "2.4.0",
"autosize": "4.0.2", "autosize": "4.0.2",
"autwh": "0.1.0", "autwh": "0.1.0",
@@ -109,20 +108,20 @@
"chalk": "2.4.2", "chalk": "2.4.2",
"commander": "2.19.0", "commander": "2.19.0",
"crc-32": "1.2.0", "crc-32": "1.2.0",
"css-loader": "2.1.0", "css-loader": "2.1.1",
"cssnano": "4.1.10", "cssnano": "4.1.10",
"dateformat": "3.0.3", "dateformat": "3.0.3",
"deep-equal": "1.0.1", "deep-equal": "1.0.1",
"deepcopy": "0.6.3", "deepcopy": "0.6.3",
"diskusage": "1.0.0", "diskusage": "1.0.0",
"double-ended-queue": "2.1.0-0", "double-ended-queue": "2.1.0-0",
"elasticsearch": "15.3.1", "elasticsearch": "15.4.1",
"emojilib": "2.4.0", "emojilib": "2.4.0",
"escape-regexp": "0.0.1", "escape-regexp": "0.0.1",
"eslint": "5.15.0", "eslint": "5.15.1",
"eslint-plugin-vue": "5.2.2", "eslint-plugin-vue": "5.2.2",
"eventemitter3": "3.1.0", "eventemitter3": "3.1.0",
"feed": "2.0.2", "feed": "2.0.4",
"file-type": "10.9.0", "file-type": "10.9.0",
"fuckadblock": "3.2.1", "fuckadblock": "3.2.1",
"gulp": "4.0.0", "gulp": "4.0.0",
@@ -131,20 +130,20 @@
"gulp-mocha": "6.0.0", "gulp-mocha": "6.0.0",
"gulp-rename": "1.4.0", "gulp-rename": "1.4.0",
"gulp-replace": "1.0.0", "gulp-replace": "1.0.0",
"gulp-sourcemaps": "2.6.4", "gulp-sourcemaps": "2.6.5",
"gulp-stylus": "2.7.0", "gulp-stylus": "2.7.0",
"gulp-tslint": "8.1.3", "gulp-tslint": "8.1.4",
"gulp-typescript": "5.0.0", "gulp-typescript": "5.0.0",
"gulp-uglify": "3.0.1", "gulp-uglify": "3.0.2",
"gulp-util": "3.0.8", "gulp-util": "3.0.8",
"hard-source-webpack-plugin": "0.13.1", "hard-source-webpack-plugin": "0.13.1",
"html-minifier": "3.5.21", "html-minifier": "3.5.21",
"http-signature": "1.2.0", "http-signature": "1.2.0",
"insert-text-at-cursor": "0.1.2", "insert-text-at-cursor": "0.1.2",
"is-root": "2.0.0", "is-root": "2.0.0",
"is-svg": "3.0.0", "is-svg": "4.0.0",
"js-yaml": "3.12.1", "js-yaml": "3.12.2",
"jsdom": "13.2.0", "jsdom": "14.0.0",
"json5": "2.1.0", "json5": "2.1.0",
"json5-loader": "1.0.1", "json5-loader": "1.0.1",
"katex": "0.10.1", "katex": "0.10.1",
@@ -189,13 +188,13 @@
"pug": "2.0.3", "pug": "2.0.3",
"punycode": "2.1.1", "punycode": "2.1.1",
"qrcode": "1.3.3", "qrcode": "1.3.3",
"randomcolor": "0.5.3", "randomcolor": "0.5.4",
"ratelimiter": "3.2.0", "ratelimiter": "3.3.0",
"recaptcha-promise": "0.1.3", "recaptcha-promise": "0.1.3",
"reconnecting-websocket": "4.1.10", "reconnecting-websocket": "4.1.10",
"redis": "2.8.0", "redis": "2.8.0",
"request": "2.88.0", "request": "2.88.0",
"request-promise-native": "1.0.5", "request-promise-native": "1.0.7",
"request-stats": "3.0.0", "request-stats": "3.0.0",
"rimraf": "2.6.3", "rimraf": "2.6.3",
"rndstr": "1.0.0", "rndstr": "1.0.0",
@@ -210,14 +209,14 @@
"stylus": "0.54.5", "stylus": "0.54.5",
"stylus-loader": "3.0.2", "stylus-loader": "3.0.2",
"summaly": "2.2.0", "summaly": "2.2.0",
"systeminformation": "4.0.14", "systeminformation": "4.0.16",
"syuilo-password-strength": "0.0.1", "syuilo-password-strength": "0.0.1",
"terser-webpack-plugin": "1.2.3", "terser-webpack-plugin": "1.2.3",
"textarea-caret": "3.1.0", "textarea-caret": "3.1.0",
"tinycolor2": "1.4.1", "tinycolor2": "1.4.1",
"tmp": "0.0.33", "tmp": "0.0.33",
"ts-loader": "5.3.3", "ts-loader": "5.3.3",
"ts-node": "8.0.2", "ts-node": "8.0.3",
"tslint": "5.13.1", "tslint": "5.13.1",
"tslint-sonarts": "1.9.0", "tslint-sonarts": "1.9.0",
"typescript": "3.3.3333", "typescript": "3.3.3333",
@@ -232,7 +231,7 @@
"vue-color": "2.7.0", "vue-color": "2.7.0",
"vue-content-loading": "1.5.3", "vue-content-loading": "1.5.3",
"vue-cropperjs": "3.0.0", "vue-cropperjs": "3.0.0",
"vue-i18n": "8.8.2", "vue-i18n": "8.9.0",
"vue-js-modal": "1.3.28", "vue-js-modal": "1.3.28",
"vue-json-pretty": "1.4.1", "vue-json-pretty": "1.4.1",
"vue-loader": "15.7.0", "vue-loader": "15.7.0",
@@ -241,9 +240,9 @@
"vue-router": "3.0.2", "vue-router": "3.0.2",
"vue-sequential-entrance": "1.1.3", "vue-sequential-entrance": "1.1.3",
"vue-style-loader": "4.1.2", "vue-style-loader": "4.1.2",
"vue-svg-inline-loader": "1.2.12", "vue-svg-inline-loader": "1.2.13",
"vue-template-compiler": "2.6.8", "vue-template-compiler": "2.6.8",
"vuedraggable": "2.18.1", "vuedraggable": "2.19.2",
"vuewordcloud": "18.7.11", "vuewordcloud": "18.7.11",
"vuex": "3.1.0", "vuex": "3.1.0",
"vuex-persistedstate": "2.5.4", "vuex-persistedstate": "2.5.4",
@@ -252,7 +251,7 @@
"webpack": "4.28.4", "webpack": "4.28.4",
"webpack-cli": "3.2.3", "webpack-cli": "3.2.3",
"websocket": "1.0.28", "websocket": "1.0.28",
"ws": "6.1.4", "ws": "6.2.0",
"xev": "2.0.1" "xev": "2.0.1"
} }
} }

View File

@@ -181,7 +181,12 @@ export default Vue.extend({
}, },
grid: { grid: {
clipMarkers: false, clipMarkers: false,
borderColor: 'rgba(0, 0, 0, 0.1)' borderColor: 'rgba(0, 0, 0, 0.1)',
xaxis: {
lines: {
show: true,
}
},
}, },
stroke: { stroke: {
curve: 'straight', curve: 'straight',

View File

@@ -0,0 +1,196 @@
<template>
<div class="mzxlfysy">
<div>
<header>
<span><fa :icon="faInbox"/> In</span>
<span v-if="latestStats">{{ latestStats.inbox.activeSincePrevTick | number }} / {{ latestStats.inbox.delayed | number }}</span>
</header>
<div ref="in"></div>
</div>
<div>
<header>
<span><fa :icon="faPaperPlane"/> Out</span>
<span v-if="latestStats">{{ latestStats.deliver.activeSincePrevTick | number }} / {{ latestStats.deliver.delayed | number }}</span>
</header>
<div ref="out"></div>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { faInbox } from '@fortawesome/free-solid-svg-icons';
import { faPaperPlane } from '@fortawesome/free-regular-svg-icons';
import ApexCharts from 'apexcharts';
const limit = 150;
export default Vue.extend({
data() {
return {
stats: [],
inChart: null,
outChart: null,
faInbox, faPaperPlane
};
},
computed: {
latestStats(): any {
return this.stats[this.stats.length - 1];
}
},
watch: {
stats(stats) {
this.inChart.updateSeries([{
data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.inbox.active }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed }))
}]);
this.outChart.updateSeries([{
data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.deliver.active }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting }))
}, {
data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed }))
}]);
}
},
mounted() {
const chartOpts = {
chart: {
type: 'area',
height: 200,
animations: {
dynamicAnimation: {
enabled: false
}
},
toolbar: {
show: false
},
zoom: {
enabled: false
}
},
dataLabels: {
enabled: false
},
grid: {
clipMarkers: false,
borderColor: 'rgba(0, 0, 0, 0.1)'
},
stroke: {
curve: 'straight',
width: 2
},
tooltip: {
enabled: false
},
legend: {
show: false
},
colors: ['#00E396', '#00BCD4', '#FFB300', '#e53935'],
series: [{ data: [] }, { data: [] }, { data: [] }, { data: [] }] as any,
xaxis: {
type: 'numeric',
labels: {
show: false
},
tooltip: {
enabled: false
}
},
yaxis: {
show: false,
min: 0,
}
};
this.inChart = new ApexCharts(this.$refs.in, chartOpts);
this.outChart = new ApexCharts(this.$refs.out, chartOpts);
this.inChart.render();
this.outChart.render();
const connection = this.$root.stream.useSharedConnection('queueStats');
connection.on('stats', this.onStats);
connection.on('statsLog', this.onStatsLog);
connection.send('requestLog', {
id: Math.random().toString().substr(2, 8),
length: limit
});
this.$once('hook:beforeDestroy', () => {
connection.dispose();
this.inChart.destroy();
this.outChart.destroy();
});
},
methods: {
onStats(stats) {
this.stats.push(stats);
if (this.stats.length > limit) this.stats.shift();
},
onStatsLog(statsLog) {
for (const stats of statsLog.reverse()) {
this.onStats(stats);
}
}
}
});
</script>
<style lang="stylus" scoped>
.mzxlfysy
display flex
> div
display block
flex 1
padding 20px 12px 0 12px
box-shadow 0 2px 4px rgba(0, 0, 0, 0.1)
background var(--face)
border-radius 8px
&:first-child
margin-right 16px
> header
display flex
padding 0 8px
margin-bottom -16px
color var(--adminDashboardCardFg)
font-size 14px
> span
&:last-child
margin-left auto
opacity 0.7
> span
opacity 0.7
> div
margin-bottom -10px
@media (max-width 1000px)
display block
margin-bottom 26px
> div
&:first-child
margin-right 0
margin-bottom 26px
</style>

View File

@@ -73,6 +73,10 @@
<x-charts ref="charts"/> <x-charts ref="charts"/>
</div> </div>
<div class="queue">
<x-queue/>
</div>
<div class="cpu-memory"> <div class="cpu-memory">
<x-cpu-memory :connection="connection"/> <x-cpu-memory :connection="connection"/>
</div> </div>
@@ -86,9 +90,10 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../i18n'; import i18n from '../../i18n';
import XCpuMemory from "./cpu-memory.vue"; import XCpuMemory from "./dashboard.cpu-memory.vue";
import XCharts from "./charts.vue"; import XQueue from "./dashboard.queue-charts.vue";
import XApLog from "./ap-log.vue"; import XCharts from "./dashboard.charts.vue";
import XApLog from "./dashboard.ap-log.vue";
import { faDatabase } from '@fortawesome/free-solid-svg-icons'; import { faDatabase } from '@fortawesome/free-solid-svg-icons';
import MarqueeText from 'vue-marquee-text-component'; import MarqueeText from 'vue-marquee-text-component';
import randomColor from 'randomcolor'; import randomColor from 'randomcolor';
@@ -98,6 +103,7 @@ export default Vue.extend({
components: { components: {
XCpuMemory, XCpuMemory,
XQueue,
XCharts, XCharts,
XApLog, XApLog,
MarqueeText MarqueeText
@@ -274,6 +280,9 @@ export default Vue.extend({
> .charts > .charts
margin-bottom 16px margin-bottom 16px
> .queue
margin-bottom 16px
> .cpu-memory > .cpu-memory
margin-bottom 16px margin-bottom 16px

View File

@@ -6,6 +6,7 @@
<ui-input :value="host" readonly>{{ $t('host') }}</ui-input> <ui-input :value="host" readonly>{{ $t('host') }}</ui-input>
<ui-input v-model="name">{{ $t('instance-name') }}</ui-input> <ui-input v-model="name">{{ $t('instance-name') }}</ui-input>
<ui-textarea v-model="description">{{ $t('instance-description') }}</ui-textarea> <ui-textarea v-model="description">{{ $t('instance-description') }}</ui-textarea>
<ui-input v-model="iconUrl"><template #icon><fa icon="link"/></template>{{ $t('icon-url') }}</ui-input>
<ui-input v-model="mascotImageUrl"><template #icon><fa icon="link"/></template>{{ $t('logo-url') }}</ui-input> <ui-input v-model="mascotImageUrl"><template #icon><fa icon="link"/></template>{{ $t('logo-url') }}</ui-input>
<ui-input v-model="bannerUrl"><template #icon><fa icon="link"/></template>{{ $t('banner-url') }}</ui-input> <ui-input v-model="bannerUrl"><template #icon><fa icon="link"/></template>{{ $t('banner-url') }}</ui-input>
<ui-input v-model="errorImageUrl"><template #icon><fa icon="link"/></template>{{ $t('error-image-url') }}</ui-input> <ui-input v-model="errorImageUrl"><template #icon><fa icon="link"/></template>{{ $t('error-image-url') }}</ui-input>
@@ -157,6 +158,7 @@ export default Vue.extend({
mascotImageUrl: null, mascotImageUrl: null,
bannerUrl: null, bannerUrl: null,
errorImageUrl: null, errorImageUrl: null,
iconUrl: null,
name: null, name: null,
description: null, description: null,
languages: null, languages: null,
@@ -207,6 +209,7 @@ export default Vue.extend({
this.mascotImageUrl = meta.mascotImageUrl; this.mascotImageUrl = meta.mascotImageUrl;
this.bannerUrl = meta.bannerUrl; this.bannerUrl = meta.bannerUrl;
this.errorImageUrl = meta.errorImageUrl; this.errorImageUrl = meta.errorImageUrl;
this.iconUrl = meta.iconUrl;
this.name = meta.name; this.name = meta.name;
this.description = meta.description; this.description = meta.description;
this.languages = meta.langs.join(' '); this.languages = meta.langs.join(' ');
@@ -267,6 +270,7 @@ export default Vue.extend({
mascotImageUrl: this.mascotImageUrl, mascotImageUrl: this.mascotImageUrl,
bannerUrl: this.bannerUrl, bannerUrl: this.bannerUrl,
errorImageUrl: this.errorImageUrl, errorImageUrl: this.errorImageUrl,
iconUrl: this.iconUrl,
name: this.name, name: this.name,
description: this.description, description: this.description,
langs: this.languages.split(' '), langs: this.languages.split(' '),

View File

@@ -1,33 +1,57 @@
<template> <template>
<div> <div>
<ui-card> <ui-card>
<template #title><fa :icon="faTasks"/> {{ $t('title') }}</template> <template #title><fa :icon="faChartBar"/> {{ $t('title') }}</template>
<section class="wptihjuy"> <section class="wptihjuy">
<header><fa :icon="faPaperPlane"/> Deliver</header> <header><fa :icon="faPaperPlane"/> Deliver</header>
<ui-info warn v-if="latestStats && latestStats.deliver.waiting > 0">The queue is jammed.</ui-info>
<ui-horizon-group inputs v-if="latestStats" class="fit-bottom"> <ui-horizon-group inputs v-if="latestStats" class="fit-bottom">
<ui-input :value="latestStats.deliver.waiting | number" type="text" readonly> <ui-input :value="latestStats.deliver.activeSincePrevTick | number" type="text" readonly>
<span>Waiting</span> <span>Process</span>
</ui-input> <template #prefix><fa :icon="fasPlayCircle"/></template>
<ui-input :value="latestStats.deliver.delayed | number" type="text" readonly> <template #suffix>jobs/tick</template>
<span>Delayed</span>
</ui-input> </ui-input>
<ui-input :value="latestStats.deliver.active | number" type="text" readonly> <ui-input :value="latestStats.deliver.active | number" type="text" readonly>
<span>Active</span> <span>Active</span>
<template #prefix><fa :icon="farPlayCircle"/></template>
<template #suffix>jobs</template>
</ui-input>
<ui-input :value="latestStats.deliver.waiting | number" type="text" readonly>
<span>Waiting</span>
<template #prefix><fa :icon="faStopCircle"/></template>
<template #suffix>jobs</template>
</ui-input>
<ui-input :value="latestStats.deliver.delayed | number" type="text" readonly>
<span>Delayed</span>
<template #prefix><fa :icon="faStopwatch"/></template>
<template #suffix>jobs</template>
</ui-input> </ui-input>
</ui-horizon-group> </ui-horizon-group>
<div ref="deliverChart" class="chart"></div> <div ref="deliverChart" class="chart"></div>
</section> </section>
<section class="wptihjuy"> <section class="wptihjuy">
<header><fa :icon="faInbox"/> Inbox</header> <header><fa :icon="faInbox"/> Inbox</header>
<ui-info warn v-if="latestStats && latestStats.inbox.waiting > 0">The queue is jammed.</ui-info>
<ui-horizon-group inputs v-if="latestStats" class="fit-bottom"> <ui-horizon-group inputs v-if="latestStats" class="fit-bottom">
<ui-input :value="latestStats.inbox.waiting | number" type="text" readonly> <ui-input :value="latestStats.inbox.activeSincePrevTick | number" type="text" readonly>
<span>Waiting</span> <span>Process</span>
</ui-input> <template #prefix><fa :icon="fasPlayCircle"/></template>
<ui-input :value="latestStats.inbox.delayed | number" type="text" readonly> <template #suffix>jobs/tick</template>
<span>Delayed</span>
</ui-input> </ui-input>
<ui-input :value="latestStats.inbox.active | number" type="text" readonly> <ui-input :value="latestStats.inbox.active | number" type="text" readonly>
<span>Active</span> <span>Active</span>
<template #prefix><fa :icon="farPlayCircle"/></template>
<template #suffix>jobs</template>
</ui-input>
<ui-input :value="latestStats.inbox.waiting | number" type="text" readonly>
<span>Waiting</span>
<template #prefix><fa :icon="faStopCircle"/></template>
<template #suffix>jobs</template>
</ui-input>
<ui-input :value="latestStats.inbox.delayed | number" type="text" readonly>
<span>Delayed</span>
<template #prefix><fa :icon="faStopwatch"/></template>
<template #suffix>jobs</template>
</ui-input> </ui-input>
</ui-horizon-group> </ui-horizon-group>
<div ref="inboxChart" class="chart"></div> <div ref="inboxChart" class="chart"></div>
@@ -36,6 +60,35 @@
<ui-button @click="removeAllJobs">{{ $t('remove-all-jobs') }}</ui-button> <ui-button @click="removeAllJobs">{{ $t('remove-all-jobs') }}</ui-button>
</section> </section>
</ui-card> </ui-card>
<ui-card>
<template #title><fa :icon="faTasks"/> {{ $t('jobs') }}</template>
<section class="fit-top">
<ui-horizon-group inputs>
<ui-select v-model="domain">
<template #label>{{ $t('queue') }}</template>
<option value="deliver">{{ $t('domains.deliver') }}</option>
<option value="inbox">{{ $t('domains.inbox') }}</option>
</ui-select>
<ui-select v-model="state">
<template #label>{{ $t('state') }}</template>
<option value="delayed">{{ $t('states.delayed') }}</option>
</ui-select>
</ui-horizon-group>
<sequential-entrance animation="entranceFromTop" delay="25">
<div class="xvvuvgsv" v-for="job in jobs">
<b>{{ job.id }}</b>
<template v-if="domain === 'deliver'">
<span>{{ job.data.to }}</span>
</template>
<template v-if="domain === 'inbox'">
<span>{{ job.activity.id }}</span>
</template>
</div>
</sequential-entrance>
<ui-info v-if="jobs.length == jobsLimit">{{ $t('result-is-truncated', { n: jobsLimit }) }}</ui-info>
</section>
</ui-card>
</div> </div>
</template> </template>
@@ -44,8 +97,10 @@ import Vue from 'vue';
import i18n from '../../i18n'; import i18n from '../../i18n';
import ApexCharts from 'apexcharts'; import ApexCharts from 'apexcharts';
import * as tinycolor from 'tinycolor2'; import * as tinycolor from 'tinycolor2';
import { faTasks, faInbox } from '@fortawesome/free-solid-svg-icons'; import { faTasks, faInbox, faStopwatch, faPlayCircle as fasPlayCircle } from '@fortawesome/free-solid-svg-icons';
import { faPaperPlane } from '@fortawesome/free-regular-svg-icons'; import { faPaperPlane, faStopCircle, faPlayCircle as farPlayCircle, faChartBar } from '@fortawesome/free-regular-svg-icons';
const limit = 150;
export default Vue.extend({ export default Vue.extend({
i18n: i18n('admin/views/queue.vue'), i18n: i18n('admin/views/queue.vue'),
@@ -55,7 +110,11 @@ export default Vue.extend({
stats: [], stats: [],
deliverChart: null, deliverChart: null,
inboxChart: null, inboxChart: null,
faTasks, faPaperPlane, faInbox jobs: [],
jobsLimit: 50,
domain: 'deliver',
state: 'delayed',
faTasks, faPaperPlane, faInbox, faStopwatch, faStopCircle, farPlayCircle, fasPlayCircle, faChartBar
}; };
}, },
@@ -68,31 +127,59 @@ export default Vue.extend({
watch: { watch: {
stats(stats) { stats(stats) {
this.inboxChart.updateSeries([{ this.inboxChart.updateSeries([{
name: 'Active', name: 'Process',
type: 'area',
data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick })) data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick }))
}, {
name: 'Active',
type: 'area',
data: stats.map((x, i) => ({ x: i, y: x.inbox.active }))
}, { }, {
name: 'Waiting', name: 'Waiting',
type: 'line',
data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting })) data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting }))
}, { }, {
name: 'Delayed', name: 'Delayed',
type: 'line',
data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed })) data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed }))
}]); }]);
this.deliverChart.updateSeries([{ this.deliverChart.updateSeries([{
name: 'Active', name: 'Process',
type: 'area',
data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick })) data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick }))
}, {
name: 'Active',
type: 'area',
data: stats.map((x, i) => ({ x: i, y: x.deliver.active }))
}, { }, {
name: 'Waiting', name: 'Waiting',
type: 'line',
data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting })) data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting }))
}, { }, {
name: 'Delayed', name: 'Delayed',
type: 'line',
data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed })) data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed }))
}]); }]);
} },
domain() {
this.jobs = [];
this.fetchJobs();
},
state() {
this.jobs = [];
this.fetchJobs();
},
}, },
mounted() { mounted() {
const chartOpts = { this.fetchJobs();
const chartOpts = id => ({
chart: { chart: {
id,
group: 'queue',
type: 'area', type: 'area',
height: 200, height: 200,
animations: { animations: {
@@ -112,7 +199,12 @@ export default Vue.extend({
}, },
grid: { grid: {
clipMarkers: false, clipMarkers: false,
borderColor: 'rgba(0, 0, 0, 0.1)' borderColor: 'rgba(0, 0, 0, 0.1)',
xaxis: {
lines: {
show: true,
}
},
}, },
stroke: { stroke: {
curve: 'straight', curve: 'straight',
@@ -127,7 +219,7 @@ export default Vue.extend({
}, },
}, },
series: [] as any, series: [] as any,
colors: ['#00BCD4', '#FFEB3B', '#e53935'], colors: ['#00E396', '#00BCD4', '#FFB300', '#e53935'],
xaxis: { xaxis: {
type: 'numeric', type: 'numeric',
labels: { labels: {
@@ -141,10 +233,10 @@ export default Vue.extend({
show: false, show: false,
min: 0, min: 0,
} }
}; });
this.inboxChart = new ApexCharts(this.$refs.inboxChart, chartOpts); this.inboxChart = new ApexCharts(this.$refs.inboxChart, chartOpts('a'));
this.deliverChart = new ApexCharts(this.$refs.deliverChart, chartOpts); this.deliverChart = new ApexCharts(this.$refs.deliverChart, chartOpts('b'));
this.inboxChart.render(); this.inboxChart.render();
this.deliverChart.render(); this.deliverChart.render();
@@ -154,7 +246,7 @@ export default Vue.extend({
connection.on('statsLog', this.onStatsLog); connection.on('statsLog', this.onStatsLog);
connection.send('requestLog', { connection.send('requestLog', {
id: Math.random().toString().substr(2, 8), id: Math.random().toString().substr(2, 8),
length: 100 length: limit
}); });
this.$once('hook:beforeDestroy', () => { this.$once('hook:beforeDestroy', () => {
@@ -184,14 +276,24 @@ export default Vue.extend({
onStats(stats) { onStats(stats) {
this.stats.push(stats); this.stats.push(stats);
if (this.stats.length > 100) this.stats.shift(); if (this.stats.length > limit) this.stats.shift();
}, },
onStatsLog(statsLog) { onStatsLog(statsLog) {
for (const stats of statsLog.reverse()) { for (const stats of statsLog.reverse()) {
this.onStats(stats); this.onStats(stats);
} }
} },
fetchJobs() {
this.$root.api('admin/queue/jobs', {
domain: this.domain,
state: this.state,
limit: this.jobsLimit
}).then(jobs => {
this.jobs = jobs;
});
},
} }
}); });
</script> </script>
@@ -200,5 +302,10 @@ export default Vue.extend({
.wptihjuy .wptihjuy
> .chart > .chart
min-height 200px !important min-height 200px !important
margin 0 -8px
.xvvuvgsv
> b
margin-right 16px
</style> </style>

View File

@@ -69,7 +69,7 @@ export default Vue.extend({
}, },
plotOptions: { plotOptions: {
bar: { bar: {
columnWidth: '90%' columnWidth: '80%'
} }
}, },
grid: { grid: {

View File

@@ -93,12 +93,17 @@ export default Vue.extend({
}, },
plotOptions: { plotOptions: {
bar: { bar: {
columnWidth: '90%' columnWidth: '80%'
} }
}, },
grid: { grid: {
clipMarkers: false, clipMarkers: false,
borderColor: 'rgba(0, 0, 0, 0.1)' borderColor: 'rgba(0, 0, 0, 0.1)',
xaxis: {
lines: {
show: true,
}
},
}, },
tooltip: { tooltip: {
shared: true, shared: true,

View File

@@ -366,9 +366,6 @@ root(fill)
&[type='file'] &[type='file']
display none display none
&[type='number']
text-align right
> .prefix > .prefix
> .suffix > .suffix
display block display block

View File

@@ -172,7 +172,7 @@ export default Vue.extend({
}, },
plotOptions: { plotOptions: {
bar: { bar: {
columnWidth: '90%' columnWidth: '80%'
} }
}, },
grid: { grid: {

View File

@@ -28,10 +28,10 @@ export default Vue.extend({
computed: { computed: {
template(): string { template(): string {
let t = ''; let t = '';
if (this.title && this.url) t += `【[${title}](${url})】\n`; if (this.title && this.url) t += `【[${this.title}](${this.url})】\n`;
if (this.title && !this.url) t += `${title}\n`; if (this.title && !this.url) t += `${this.title}\n`;
if (this.text) t += `${text}\n`; if (this.text) t += `${this.text}\n`;
if (!this.title && this.url) t += `${url}`; if (!this.title && this.url) t += `${this.url}`;
return t.trim(); return t.trim();
} }
}, },

View File

@@ -42,13 +42,29 @@ export default define({
watch: { watch: {
stats(stats) { stats(stats) {
this.inChart.updateSeries([{ this.inChart.updateSeries([{
type: 'area',
data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick })) data: stats.map((x, i) => ({ x: i, y: x.inbox.activeSincePrevTick }))
}, { }, {
type: 'area',
data: stats.map((x, i) => ({ x: i, y: x.inbox.active }))
}, {
type: 'line',
data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting }))
}, {
type: 'line',
data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed })) data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed }))
}]); }]);
this.outChart.updateSeries([{ this.outChart.updateSeries([{
type: 'area',
data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick })) data: stats.map((x, i) => ({ x: i, y: x.deliver.activeSincePrevTick }))
}, { }, {
type: 'area',
data: stats.map((x, i) => ({ x: i, y: x.deliver.active }))
}, {
type: 'line',
data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting }))
}, {
type: 'line',
data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed })) data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed }))
}]); }]);
} }
@@ -81,11 +97,8 @@ export default define({
curve: 'straight', curve: 'straight',
width: 1 width: 1
}, },
series: [{ colors: ['#00E396', '#00BCD4', '#FFB300', '#e53935'],
data: [] as any series: [{ data: [] }, { data: [] }, { data: [] }, { data: [] }] as any,
}, {
data: [] as any
}],
yaxis: { yaxis: {
min: 0, min: 0,
} }

View File

@@ -4,7 +4,7 @@ import { deliverQueue, inboxQueue } from '../queue';
const ev = new Xev(); const ev = new Xev();
const interval = 1000; const interval = 2000;
/** /**
* Report queue stats regularly * Report queue stats regularly

View File

@@ -1,5 +1,6 @@
import { parseFragment, DefaultTreeDocumentFragment } from 'parse5'; import { parseFragment, DefaultTreeDocumentFragment } from 'parse5';
import { URL } from 'url'; import { URL } from 'url';
import { urlRegex } from './prelude';
export function fromHtml(html: string): string { export function fromHtml(html: string): string {
if (html == null) return null; if (html == null) return null;
@@ -14,7 +15,7 @@ export function fromHtml(html: string): string {
return text.trim(); return text.trim();
function getText(node: any) { function getText(node: any): string {
if (node.nodeName == '#text') return node.value; if (node.nodeName == '#text') return node.value;
if (node.childNodes) { if (node.childNodes) {
@@ -38,10 +39,11 @@ export function fromHtml(html: string): string {
const txt = getText(node); const txt = getText(node);
const rel = node.attrs.find((x: any) => x.name == 'rel'); const rel = node.attrs.find((x: any) => x.name == 'rel');
const href = node.attrs.find((x: any) => x.name == 'href'); const href = node.attrs.find((x: any) => x.name == 'href');
const isHashtag = rel && rel.value.match('tag') !== null;
// ハッシュタグ / hrefがない / txtがURL // ハッシュタグ / hrefがない / txtがURL
if ((rel && rel.value.match('tag') !== null) || !href || href.value == txt) { if (isHashtag || !href || href.value == txt) {
text += txt; text += isHashtag || txt.match(urlRegex) ? txt : `<${txt}>`;
// メンション // メンション
} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) { } else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) {
const part = txt.split('@'); const part = txt.split('@');

View File

@@ -1,5 +1,5 @@
import * as P from 'parsimmon'; import * as P from 'parsimmon';
import { createLeaf, createTree } from './types'; import { createLeaf, createTree, urlRegex } from './prelude';
import { takeWhile, cumulativeSum } from '../prelude/array'; import { takeWhile, cumulativeSum } from '../prelude/array';
import parseAcct from '../misc/acct/parse'; import parseAcct from '../misc/acct/parse';
import { toUnicode } from 'punycode'; import { toUnicode } from 'punycode';
@@ -154,9 +154,16 @@ export const mfmLanguage = P.createLanguage({
url: () => { url: () => {
return P((input, i) => { return P((input, i) => {
const text = input.substr(i); const text = input.substr(i);
const match = text.match(/^https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.,=\+\-]+/); const match = text.match(urlRegex);
if (!match) return P.makeFailure(i, 'not a url'); let url: string;
let url = match[0]; if (!match) {
const match = text.match(/^<(https?:\/\/.*?)>/);
if (!match)
return P.makeFailure(i, 'not a url');
url = match[1];
i += 2;
} else
url = match[0];
url = removeOrphanedBrackets(url); url = removeOrphanedBrackets(url);
if (url.endsWith('.')) url = url.substr(0, url.lastIndexOf('.')); if (url.endsWith('.')) url = url.substr(0, url.lastIndexOf('.'));
if (url.endsWith(',')) url = url.substr(0, url.lastIndexOf(',')); if (url.endsWith(',')) url = url.substr(0, url.lastIndexOf(','));

View File

@@ -1,6 +1,6 @@
import * as A from '../prelude/array'; import * as A from '../prelude/array';
import * as S from '../prelude/string'; import * as S from '../prelude/string';
import { MfmForest, MfmTree } from './types'; import { MfmForest, MfmTree } from './prelude';
import { createTree, createLeaf } from '../prelude/tree'; import { createTree, createLeaf } from '../prelude/tree';
function isEmptyTextTree(t: MfmTree): boolean { function isEmptyTextTree(t: MfmTree): boolean {

View File

@@ -1,5 +1,5 @@
import { mfmLanguage } from './language'; import { mfmLanguage } from './language';
import { MfmForest } from './types'; import { MfmForest } from './prelude';
import { normalize } from './normalize'; import { normalize } from './normalize';
export function parse(source: string): MfmForest { export function parse(source: string): MfmForest {

View File

@@ -35,3 +35,5 @@ export function createLeaf(type: string, props: any): MfmTree {
export function createTree(type: string, children: MfmForest, props: any): MfmTree { export function createTree(type: string, children: MfmForest, props: any): MfmTree {
return T.createTree({ type, props }, children); return T.createTree({ type, props }, children);
} }
export const urlRegex = /^https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.,=\+\-]+/;

View File

@@ -2,7 +2,7 @@ import { JSDOM } from 'jsdom';
import config from '../config'; import config from '../config';
import { INote } from '../models/note'; import { INote } from '../models/note';
import { intersperse } from '../prelude/array'; import { intersperse } from '../prelude/array';
import { MfmForest, MfmTree } from './types'; import { MfmForest, MfmTree } from './prelude';
export function toHtml(tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteUsers'] = []) { export function toHtml(tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteUsers'] = []) {
if (tokens == null) { if (tokens == null) {

View File

@@ -1,5 +1,5 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as isSvg from 'is-svg'; import isSvg from 'is-svg';
export default function(path: string) { export default function(path: string) {
try { try {

27
src/misc/convert-host.ts Normal file
View File

@@ -0,0 +1,27 @@
import config from '../config';
import { toUnicode, toASCII } from 'punycode';
import { URL } from 'url';
export function getFullApAccount(username: string, host: string) {
return host ? `${username}@${toApHost(host)}` : `${username}@${toApHost(config.host)}`;
}
export function isSelfHost(host: string) {
if (host == null) return true;
return toApHost(config.host) === toApHost(host);
}
export function extractDbHost(uri: string) {
const url = new URL(uri);
return toDbHost(url.hostname);
}
export function toDbHost(host: string) {
if (host == null) return null;
return toUnicode(host.toLowerCase());
}
export function toApHost(host: string) {
if (host == null) return null;
return toASCII(host.toLowerCase());
}

View File

@@ -1,4 +1,4 @@
import { EmojiNode, MfmForest } from '../mfm/types'; import { EmojiNode, MfmForest } from '../mfm/prelude';
import { preorderF } from '../prelude/tree'; import { preorderF } from '../prelude/tree';
import { unique } from '../prelude/array'; import { unique } from '../prelude/array';

View File

@@ -1,4 +1,4 @@
import { HashtagNode, MfmForest } from '../mfm/types'; import { HashtagNode, MfmForest } from '../mfm/prelude';
import { preorderF } from '../prelude/tree'; import { preorderF } from '../prelude/tree';
import { unique } from '../prelude/array'; import { unique } from '../prelude/array';

View File

@@ -1,6 +1,6 @@
// test is located in test/extract-mentions // test is located in test/extract-mentions
import { MentionNode, MfmForest } from '../mfm/types'; import { MentionNode, MfmForest } from '../mfm/prelude';
import { preorderF } from '../prelude/tree'; import { preorderF } from '../prelude/tree';
export default function(mfmForest: MfmForest): MentionNode['props'][] { export default function(mfmForest: MfmForest): MentionNode['props'][] {

View File

@@ -198,6 +198,7 @@ export type IMeta = {
mascotImageUrl?: string; mascotImageUrl?: string;
bannerUrl?: string; bannerUrl?: string;
errorImageUrl?: string; errorImageUrl?: string;
iconUrl?: string;
cacheRemoteFiles?: boolean; cacheRemoteFiles?: boolean;

View File

@@ -41,6 +41,7 @@ export type INote = {
replyId: mongo.ObjectID; replyId: mongo.ObjectID;
renoteId: mongo.ObjectID; renoteId: mongo.ObjectID;
poll: IPoll; poll: IPoll;
name?: string;
text: string; text: string;
tags: string[]; tags: string[];
tagsLower: string[]; tagsLower: string[];
@@ -391,6 +392,10 @@ export const pack = async (
} }
//#endregion //#endregion
if (_note.name) {
_note.text = `${_note.name}\n${_note.text}`;
}
if (_note.user.isCat && _note.text) { if (_note.user.isCat && _note.text) {
_note.text = (_note.text _note.text = (_note.text
// ja-JP // ja-JP

View File

@@ -20,6 +20,7 @@ User.createIndex('createdAt');
User.createIndex('updatedAt'); User.createIndex('updatedAt');
User.createIndex('followersCount'); User.createIndex('followersCount');
User.createIndex('tags'); User.createIndex('tags');
User.createIndex('isSuspended');
User.createIndex('username'); User.createIndex('username');
User.createIndex('usernameLower'); User.createIndex('usernameLower');
User.createIndex('host'); User.createIndex('host');

View File

@@ -178,10 +178,10 @@ export function destroy() {
deliverQueue.once('cleaned', (jobs, status) => { deliverQueue.once('cleaned', (jobs, status) => {
deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
}); });
deliverQueue.clean(0, 'wait'); deliverQueue.clean(0, 'delayed');
inboxQueue.once('cleaned', (jobs, status) => { inboxQueue.once('cleaned', (jobs, status) => {
inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`); inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
}); });
inboxQueue.clean(0, 'wait'); inboxQueue.clean(0, 'delayed');
} }

View File

@@ -8,7 +8,7 @@ import addFile from '../../../services/drive/add-file';
import User from '../../../models/user'; import User from '../../../models/user';
import dateFormat = require('dateformat'); import dateFormat = require('dateformat');
import Blocking from '../../../models/blocking'; import Blocking from '../../../models/blocking';
import config from '../../../config'; import { getFullApAccount } from '../../../misc/convert-host';
const logger = queueLogger.createSubLogger('export-blocking'); const logger = queueLogger.createSubLogger('export-blocking');
@@ -56,7 +56,7 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> {
for (const block of blockings) { for (const block of blockings) {
const u = await User.findOne({ _id: block.blockeeId }, { fields: { username: true, host: true } }); const u = await User.findOne({ _id: block.blockeeId }, { fields: { username: true, host: true } });
const content = u.host ? `${u.username}@${u.host}` : `${u.username}@${config.host}`; const content = getFullApAccount(u.username, u.host);
await new Promise((res, rej) => { await new Promise((res, rej) => {
stream.write(content + '\n', err => { stream.write(content + '\n', err => {
if (err) { if (err) {

View File

@@ -8,7 +8,7 @@ import addFile from '../../../services/drive/add-file';
import User from '../../../models/user'; import User from '../../../models/user';
import dateFormat = require('dateformat'); import dateFormat = require('dateformat');
import Following from '../../../models/following'; import Following from '../../../models/following';
import config from '../../../config'; import { getFullApAccount } from '../../../misc/convert-host';
const logger = queueLogger.createSubLogger('export-following'); const logger = queueLogger.createSubLogger('export-following');
@@ -56,7 +56,7 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> {
for (const following of followings) { for (const following of followings) {
const u = await User.findOne({ _id: following.followeeId }, { fields: { username: true, host: true } }); const u = await User.findOne({ _id: following.followeeId }, { fields: { username: true, host: true } });
const content = u.host ? `${u.username}@${u.host}` : `${u.username}@${config.host}`; const content = getFullApAccount(u.username, u.host);
await new Promise((res, rej) => { await new Promise((res, rej) => {
stream.write(content + '\n', err => { stream.write(content + '\n', err => {
if (err) { if (err) {

View File

@@ -8,7 +8,7 @@ import addFile from '../../../services/drive/add-file';
import User from '../../../models/user'; import User from '../../../models/user';
import dateFormat = require('dateformat'); import dateFormat = require('dateformat');
import Mute from '../../../models/mute'; import Mute from '../../../models/mute';
import config from '../../../config'; import { getFullApAccount } from '../../../misc/convert-host';
const logger = queueLogger.createSubLogger('export-mute'); const logger = queueLogger.createSubLogger('export-mute');
@@ -56,7 +56,7 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> {
for (const mute of mutes) { for (const mute of mutes) {
const u = await User.findOne({ _id: mute.muteeId }, { fields: { username: true, host: true } }); const u = await User.findOne({ _id: mute.muteeId }, { fields: { username: true, host: true } });
const content = u.host ? `${u.username}@${u.host}` : `${u.username}@${config.host}`; const content = getFullApAccount(u.username, u.host);
await new Promise((res, rej) => { await new Promise((res, rej) => {
stream.write(content + '\n', err => { stream.write(content + '\n', err => {
if (err) { if (err) {

View File

@@ -7,8 +7,8 @@ import { queueLogger } from '../../logger';
import addFile from '../../../services/drive/add-file'; import addFile from '../../../services/drive/add-file';
import User from '../../../models/user'; import User from '../../../models/user';
import dateFormat = require('dateformat'); import dateFormat = require('dateformat');
import config from '../../../config';
import UserList from '../../../models/user-list'; import UserList from '../../../models/user-list';
import { getFullApAccount } from '../../../misc/convert-host';
const logger = queueLogger.createSubLogger('export-user-lists'); const logger = queueLogger.createSubLogger('export-user-lists');
@@ -46,7 +46,7 @@ export async function exportUserLists(job: Bull.Job, done: any): Promise<void> {
}); });
for (const u of users) { for (const u of users) {
const acct = u.host ? `${u.username}@${u.host}` : `${u.username}@${config.host}`; const acct = getFullApAccount(u.username, u.host);
const content = `${list.title},${acct}`; const content = `${list.title},${acct}`;
await new Promise((res, rej) => { await new Promise((res, rej) => {
stream.write(content + '\n', err => { stream.write(content + '\n', err => {

View File

@@ -3,13 +3,13 @@ import * as mongo from 'mongodb';
import { queueLogger } from '../../logger'; import { queueLogger } from '../../logger';
import User from '../../../models/user'; import User from '../../../models/user';
import config from '../../../config';
import follow from '../../../services/following/create'; import follow from '../../../services/following/create';
import DriveFile from '../../../models/drive-file'; import DriveFile from '../../../models/drive-file';
import { getOriginalUrl } from '../../../misc/get-drive-file-url'; import { getOriginalUrl } from '../../../misc/get-drive-file-url';
import parseAcct from '../../../misc/acct/parse'; import parseAcct from '../../../misc/acct/parse';
import resolveUser from '../../../remote/resolve-user'; import resolveUser from '../../../remote/resolve-user';
import { downloadTextFile } from '../../../misc/download-text-file'; import { downloadTextFile } from '../../../misc/download-text-file';
import { isSelfHost, toDbHost } from '../../../misc/convert-host';
const logger = queueLogger.createSubLogger('import-following'); const logger = queueLogger.createSubLogger('import-following');
@@ -31,11 +31,11 @@ export async function importFollowing(job: Bull.Job, done: any): Promise<void> {
for (const line of csv.trim().split('\n')) { for (const line of csv.trim().split('\n')) {
const { username, host } = parseAcct(line.trim()); const { username, host } = parseAcct(line.trim());
let target = host === config.host ? await User.findOne({ let target = isSelfHost(host) ? await User.findOne({
host: null, host: null,
usernameLower: username.toLowerCase() usernameLower: username.toLowerCase()
}) : await User.findOne({ }) : await User.findOne({
host: host, host: toDbHost(host),
usernameLower: username.toLowerCase() usernameLower: username.toLowerCase()
}); });

View File

@@ -3,7 +3,6 @@ import * as mongo from 'mongodb';
import { queueLogger } from '../../logger'; import { queueLogger } from '../../logger';
import User from '../../../models/user'; import User from '../../../models/user';
import config from '../../../config';
import UserList from '../../../models/user-list'; import UserList from '../../../models/user-list';
import DriveFile from '../../../models/drive-file'; import DriveFile from '../../../models/drive-file';
import { getOriginalUrl } from '../../../misc/get-drive-file-url'; import { getOriginalUrl } from '../../../misc/get-drive-file-url';
@@ -11,6 +10,7 @@ import parseAcct from '../../../misc/acct/parse';
import resolveUser from '../../../remote/resolve-user'; import resolveUser from '../../../remote/resolve-user';
import { pushUserToUserList } from '../../../services/user-list/push'; import { pushUserToUserList } from '../../../services/user-list/push';
import { downloadTextFile } from '../../../misc/download-text-file'; import { downloadTextFile } from '../../../misc/download-text-file';
import { isSelfHost, toDbHost } from '../../../misc/convert-host';
const logger = queueLogger.createSubLogger('import-user-lists'); const logger = queueLogger.createSubLogger('import-user-lists');
@@ -47,11 +47,11 @@ export async function importUserLists(job: Bull.Job, done: any): Promise<void> {
}); });
} }
let target = host === config.host ? await User.findOne({ let target = isSelfHost(host) ? await User.findOne({
host: null, host: null,
usernameLower: username.toLowerCase() usernameLower: username.toLowerCase()
}) : await User.findOne({ }) : await User.findOne({
host: host, host: toDbHost(host),
usernameLower: username.toLowerCase() usernameLower: username.toLowerCase()
}); });

View File

@@ -24,10 +24,8 @@ export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> =>
switch (object.type) { switch (object.type) {
case 'Note': case 'Note':
announceNote(resolver, actor, activity, object as INote);
break;
case 'Question': case 'Question':
case 'Article':
announceNote(resolver, actor, activity, object as INote); announceNote(resolver, actor, activity, object as INote);
break; break;

View File

@@ -5,6 +5,8 @@ import { IAnnounce, INote } from '../../type';
import { fetchNote, resolveNote } from '../../models/note'; import { fetchNote, resolveNote } from '../../models/note';
import { resolvePerson } from '../../models/person'; import { resolvePerson } from '../../models/person';
import { apLogger } from '../../logger'; import { apLogger } from '../../logger';
import { extractDbHost } from '../../../../misc/convert-host';
import Instance from '../../../../models/instance';
const logger = apLogger; const logger = apLogger;
@@ -23,6 +25,11 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
throw new Error('invalid announce'); throw new Error('invalid announce');
} }
// アナウンス先をブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
const instance = await Instance.findOne({ host: extractDbHost(uri) });
if (instance && instance.isBlocked) return;
// 既に同じURIを持つものが登録されていないかチェック // 既に同じURIを持つものが登録されていないかチェック
const exist = await fetchNote(uri); const exist = await fetchNote(uri);
if (exist) { if (exist) {

View File

@@ -29,10 +29,8 @@ export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
break; break;
case 'Note': case 'Note':
createNote(resolver, actor, object);
break;
case 'Question': case 'Question':
case 'Article':
createNote(resolver, actor, object); createNote(resolver, actor, object);
break; break;

View File

@@ -21,10 +21,8 @@ export default async (actor: IRemoteUser, activity: IDelete): Promise<void> => {
switch (object.type) { switch (object.type) {
case 'Note': case 'Note':
deleteNote(actor, uri);
break;
case 'Question': case 'Question':
case 'Article':
deleteNote(actor, uri); deleteNote(actor, uri);
break; break;

View File

@@ -1,3 +1,3 @@
import { remoteLogger } from "../logger"; import { remoteLogger } from '../logger';
export const apLogger = remoteLogger.createSubLogger('ap', 'magenta'); export const apLogger = remoteLogger.createSubLogger('ap', 'magenta');

View File

@@ -19,6 +19,8 @@ import vote from '../../../services/note/polls/vote';
import { apLogger } from '../logger'; import { apLogger } from '../logger';
import { IDriveFile } from '../../../models/drive-file'; import { IDriveFile } from '../../../models/drive-file';
import { deliverQuestionUpdate } from '../../../services/note/polls/update'; import { deliverQuestionUpdate } from '../../../services/note/polls/update';
import Instance from '../../../models/instance';
import { extractDbHost } from '../../../misc/convert-host';
const logger = apLogger; const logger = apLogger;
@@ -55,7 +57,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
const object: any = await resolver.resolve(value); const object: any = await resolver.resolve(value);
if (!object || !['Note', 'Question'].includes(object.type)) { if (!object || !['Note', 'Question', 'Article'].includes(object.type)) {
logger.error(`invalid note: ${value}`, { logger.error(`invalid note: ${value}`, {
resolver: { resolver: {
history: resolver.getHistory() history: resolver.getHistory()
@@ -116,7 +118,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
: []; : [];
// リプライ // リプライ
const reply = note.inReplyTo const reply: INote = note.inReplyTo
? await resolveNote(note.inReplyTo, resolver).catch(e => { ? await resolveNote(note.inReplyTo, resolver).catch(e => {
// 4xxの場合はリプライしてないことにする // 4xxの場合はリプライしてないことにする
if (e.statusCode >= 400 && e.statusCode < 500) { if (e.statusCode >= 400 && e.statusCode < 500) {
@@ -132,7 +134,15 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
let quote: INote; let quote: INote;
if (note._misskey_quote && typeof note._misskey_quote == 'string') { if (note._misskey_quote && typeof note._misskey_quote == 'string') {
quote = await resolveNote(note._misskey_quote).catch(() => null); quote = await resolveNote(note._misskey_quote).catch(e => {
// 4xxの場合は引用してないことにする
if (e.statusCode >= 400 && e.statusCode < 500) {
logger.warn(`Ignored quote target ${note.inReplyTo} - ${e.statusCode} `);
return null;
}
logger.warn(`Error in quote target ${note.inReplyTo} - ${e.statusCode || e}`);
throw e;
});
} }
const cw = note.summary === '' ? null : note.summary; const cw = note.summary === '' ? null : note.summary;
@@ -189,6 +199,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
files, files,
reply, reply,
renote: quote, renote: quote,
name: note.name,
cw, cw,
text, text,
viaMobile: false, viaMobile: false,
@@ -214,6 +225,11 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<INote> { export async function resolveNote(value: string | IObject, resolver?: Resolver): Promise<INote> {
const uri = typeof value == 'string' ? value : value.id; const uri = typeof value == 'string' ? value : value.id;
// ブロックしてたら中断
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
const instance = await Instance.findOne({ host: extractDbHost(uri) });
if (instance && instance.isBlocked) throw { statusCode: 451 };
//#region このサーバーに既に登録されていたらそれを返す //#region このサーバーに既に登録されていたらそれを返す
const exist = await fetchNote(uri); const exist = await fetchNote(uri);

View File

@@ -1,3 +1,3 @@
import Logger from "../services/logger"; import Logger from '../services/logger';
export const remoteLogger = new Logger('remote', 'cyan'); export const remoteLogger = new Logger('remote', 'cyan');

View File

@@ -0,0 +1,40 @@
import $ from 'cafy';
import define from '../../../define';
import { deliverQueue, inboxQueue } from '../../../../../queue';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
params: {
domain: {
validator: $.str,
},
state: {
validator: $.str,
},
limit: {
validator: $.optional.num,
default: 50
},
}
};
export default define(meta, async (ps) => {
const queue =
ps.domain === 'deliver' ? deliverQueue :
ps.domain === 'inbox' ? inboxQueue :
null;
const jobs = await queue.getJobs([ps.state], 0, ps.limit);
return jobs.map(job => ({
id: job.id,
data: job.data,
attempts: job.attemptsMade,
}));
});

View File

@@ -1,7 +1,9 @@
import $ from 'cafy'; import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id'; import ID, { transform } from '../../../../misc/cafy-id';
import define from '../../define'; import define from '../../define';
import User from '../../../../models/user'; import User, { IUser } from '../../../../models/user';
import Following from '../../../../models/following';
import deleteFollowing from '../../../../services/following/delete';
export const meta = { export const meta = {
desc: { desc: {
@@ -51,5 +53,25 @@ export default define(meta, async (ps) => {
} }
}); });
unFollowAll(user);
return; return;
}); });
async function unFollowAll(follower: IUser) {
const followings = await Following.find({
followerId: follower._id
});
for (const following of followings) {
const followee = await User.findOne({
_id: following.followeeId
});
if (followee == null) {
throw `Cant find followee ${following.followeeId}`;
}
await deleteFollowing(follower, followee, true);
}
}

View File

@@ -69,6 +69,13 @@ export const meta = {
} }
}, },
iconUrl: {
validator: $.optional.nullable.str,
desc: {
'ja-JP': 'インスタンスのアイコンURL'
}
},
name: { name: {
validator: $.optional.nullable.str, validator: $.optional.nullable.str,
desc: { desc: {
@@ -356,6 +363,10 @@ export default define(meta, async (ps) => {
set.bannerUrl = ps.bannerUrl; set.bannerUrl = ps.bannerUrl;
} }
if (ps.iconUrl !== undefined) {
set.iconUrl = ps.iconUrl;
}
if (ps.name !== undefined) { if (ps.name !== undefined) {
set.name = ps.name; set.name = ps.name;
} }

View File

@@ -8,6 +8,8 @@ import Note, { pack as packNote, INote } from '../../../../models/note';
import { createNote } from '../../../../remote/activitypub/models/note'; import { createNote } from '../../../../remote/activitypub/models/note';
import Resolver from '../../../../remote/activitypub/resolver'; import Resolver from '../../../../remote/activitypub/resolver';
import { ApiError } from '../../error'; import { ApiError } from '../../error';
import Instance from '../../../../models/instance';
import { extractDbHost } from '../../../../misc/convert-host';
export const meta = { export const meta = {
tags: ['federation'], tags: ['federation'],
@@ -61,6 +63,10 @@ async function fetchAny(uri: string) {
if (packed !== null) return packed; if (packed !== null) return packed;
} }
// ブロックしてたら中断
const instance = await Instance.findOne({ host: extractDbHost(uri) });
if (instance && instance.isBlocked) return null;
// URI(AP Object id)としてDB検索 // URI(AP Object id)としてDB検索
{ {
const [user, note] = await Promise.all([ const [user, note] = await Promise.all([
@@ -97,7 +103,7 @@ async function fetchAny(uri: string) {
}; };
} }
if (['Note', 'Question'].includes(object.type)) { if (['Note', 'Question', 'Article'].includes(object.type)) {
const note = await createNote(object.id); const note = await createNote(object.id);
return { return {
type: 'Note', type: 'Note',

View File

@@ -116,6 +116,7 @@ export default define(meta, async (ps, me) => {
mascotImageUrl: instance.mascotImageUrl, mascotImageUrl: instance.mascotImageUrl,
bannerUrl: instance.bannerUrl, bannerUrl: instance.bannerUrl,
errorImageUrl: instance.errorImageUrl, errorImageUrl: instance.errorImageUrl,
iconUrl: instance.iconUrl,
maxNoteTextLength: instance.maxNoteTextLength, maxNoteTextLength: instance.maxNoteTextLength,
emojis: emojis, emojis: emojis,
enableEmail: instance.enableEmail, enableEmail: instance.enableEmail,

View File

@@ -70,7 +70,8 @@ export default define(meta, async (ps, me) => {
users = await User users = await User
.find({ .find({
host: null, host: null,
usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase())) usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase())),
isSuspended: false
}, { }, {
limit: ps.limit, limit: ps.limit,
skip: ps.offset skip: ps.offset
@@ -80,7 +81,8 @@ export default define(meta, async (ps, me) => {
const otherUsers = await User const otherUsers = await User
.find({ .find({
host: { $ne: null }, host: { $ne: null },
usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase())) usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase())),
isSuspended: false
}, { }, {
limit: ps.limit - users.length limit: ps.limit - users.length
}); });

View File

@@ -1,3 +1,3 @@
import Logger from "../../services/logger"; import Logger from '../../services/logger';
export const apiLogger = new Logger('api'); export const apiLogger = new Logger('api');

View File

@@ -250,7 +250,10 @@ router.get('/reversi', async ctx => ctx.redirect(override(ctx.URL.pathname, 'gam
router.get('*', async ctx => { router.get('*', async ctx => {
const meta = await fetchMeta(); const meta = await fetchMeta();
await ctx.render('base', { await ctx.render('base', {
img: meta.bannerUrl img: meta.bannerUrl,
title: meta.name,
desc: meta.description,
icon: meta.iconUrl
}); });
ctx.set('Cache-Control', 'public, max-age=300'); ctx.set('Cache-Control', 'public, max-age=300');
}); });

View File

@@ -8,17 +8,19 @@ html
head head
meta(charset='utf-8') meta(charset='utf-8')
meta(name='application-name' content='Misskey') meta(name='application-name' content= title || 'Misskey')
meta(name='referrer' content='origin') meta(name='referrer' content='origin')
meta(property='og:site_name' content='Misskey') meta(name='theme-color' content='#105779')
meta(property='og:site_name' content= title || 'Misskey')
link(rel='icon' href= icon || '/favicon.ico')
link(rel='manifest' href='/manifest.json') link(rel='manifest' href='/manifest.json')
title title
block title block title
| Misskey = title || 'Misskey'
block desc block desc
meta(name='description' content='✨🌎✨ A federated blogging platform ✨🚀✨') meta(name='description' content= desc || '✨🌎✨ A federated blogging platform ✨🚀✨')
block meta block meta

View File

@@ -1,3 +1,3 @@
import Logger from "../logger"; import Logger from '../logger';
export const driveLogger = new Logger('drive', 'blue'); export const driveLogger = new Logger('drive', 'blue');

View File

@@ -13,7 +13,7 @@ import instanceChart from '../../services/chart/instance';
const logger = new Logger('following/delete'); const logger = new Logger('following/delete');
export default async function(follower: IUser, followee: IUser) { export default async function(follower: IUser, followee: IUser, silent = false) {
const following = await Following.findOne({ const following = await Following.findOne({
followerId: follower._id, followerId: follower._id,
followeeId: followee._id followeeId: followee._id
@@ -71,7 +71,7 @@ export default async function(follower: IUser, followee: IUser) {
perUserFollowingChart.update(follower, followee, false); perUserFollowingChart.update(follower, followee, false);
// Publish unfollow event // Publish unfollow event
if (isLocalUser(follower)) { if (!silent && isLocalUser(follower)) {
packUser(followee, follower, { packUser(followee, follower, {
detail: true detail: true
}).then(packed => publishMainStream(follower._id, 'unfollow', packed)); }).then(packed => publishMainStream(follower._id, 'unfollow', packed));

View File

@@ -91,6 +91,7 @@ class NotificationManager {
type Option = { type Option = {
createdAt?: Date; createdAt?: Date;
name?: string;
text?: string; text?: string;
reply?: INote; reply?: INote;
renote?: INote; renote?: INote;
@@ -437,6 +438,7 @@ async function insertNote(user: IUser, data: Option, tags: string[], emojis: str
fileIds: data.files ? data.files.map(file => file._id) : [], fileIds: data.files ? data.files.map(file => file._id) : [],
replyId: data.reply ? data.reply._id : null, replyId: data.reply ? data.reply._id : null,
renoteId: data.renote ? data.renote._id : null, renoteId: data.renote ? data.renote._id : null,
name: data.name,
text: data.text, text: data.text,
poll: data.poll, poll: data.poll,
cw: data.cw == null ? null : data.cw, cw: data.cw == null ? null : data.cw,

View File

@@ -12,7 +12,7 @@ import * as assert from 'assert';
import { parse, parsePlain } from '../src/mfm/parse'; import { parse, parsePlain } from '../src/mfm/parse';
import { toHtml } from '../src/mfm/toHtml'; import { toHtml } from '../src/mfm/toHtml';
import { createTree as tree, createLeaf as leaf, MfmTree } from '../src/mfm/types'; import { createTree as tree, createLeaf as leaf, MfmTree } from '../src/mfm/prelude';
import { removeOrphanedBrackets } from '../src/mfm/language'; import { removeOrphanedBrackets } from '../src/mfm/language';
function text(text: string): MfmTree { function text(text: string): MfmTree {
@@ -840,6 +840,20 @@ describe('MFM', () => {
text(')') text(')')
]); ]);
}); });
it('ignore non-ascii characters contained url without angle brackets', () => {
const tokens = parse('https://大石泉すき.example.com');
assert.deepStrictEqual(tokens, [
text('https://大石泉すき.example.com')
]);
});
it('match non-ascii characters contained url with angle brackets', () => {
const tokens = parse('<https://大石泉すき.example.com>');
assert.deepStrictEqual(tokens, [
leaf('url', { url: 'https://大石泉すき.example.com' })
]);
});
}); });
describe('link', () => { describe('link', () => {