Compare commits
44 Commits
Author | SHA1 | Date | |
---|---|---|---|
182ca5d434 | |||
facde9a75d | |||
41385640b9 | |||
7bad9db32e | |||
af66f0a497 | |||
95e1b80f41 | |||
556e2eba95 | |||
efe530cb17 | |||
34e7c99283 | |||
4157ea8bc3 | |||
550517bbf3 | |||
eb910cd8a1 | |||
75131c4e8a | |||
ee29ab95be | |||
e97951fc51 | |||
dfabdef60f | |||
5a87763193 | |||
6bb90f56fa | |||
c883ae1350 | |||
09e25e6a02 | |||
bf5d43054b | |||
63b3c65691 | |||
f193da7f67 | |||
40f38c2c0a | |||
db439ef804 | |||
56eb896a03 | |||
68d43e43b6 | |||
c60517e49a | |||
3f59d261f2 | |||
4068d220e5 | |||
18968e7208 | |||
38656103c0 | |||
0f65b1bcc5 | |||
a628821834 | |||
6ceff60c1e | |||
d762a6ce58 | |||
75a8037a46 | |||
1179920790 | |||
b323a160e3 | |||
b157e9535e | |||
7668475bd6 | |||
8bda2a1fb7 | |||
b092086b5b | |||
69a0d9034f |
28
CHANGELOG.md
28
CHANGELOG.md
@ -5,6 +5,34 @@ If you encounter any problems with updating, please try the following:
|
||||
1. `npm run clean` or `npm run cleanall`
|
||||
2. Retry update (Don't forget `npm i`)
|
||||
|
||||
10.96.0
|
||||
----------
|
||||
* 連合ユーザーの投稿に対してActivityPubオブジェクトを要求されたら元のインスタンスにリダイレクトするように
|
||||
* updatePersonを試行した時点でもlastFetchedAtを更新するように
|
||||
* 管理画面でリモートインスタンスの登録日時を表示
|
||||
* ユーザーサジェストが機能しなくなっていた問題を修正
|
||||
* 最近使ったハッシュタグ表示が機能していない問題を修正
|
||||
* バグ修正
|
||||
* デザインの調整
|
||||
|
||||
10.95.0
|
||||
----------
|
||||
* ジョブを一覧できるように
|
||||
* MFMでURLを明示する構文の追加
|
||||
* Articleタイプのアクティビティを受け入れるように
|
||||
* 凍結されたユーザーをサジェストしないように
|
||||
* ファビコンが保存されないのを修正
|
||||
* キューのジョブクリアの動作を修正
|
||||
* デザインの調整
|
||||
|
||||
10.94.0
|
||||
----------
|
||||
* Faviconを設定できるように
|
||||
* アカウントを凍結したときすべてのフォローを解除するように
|
||||
* シェアページが機能していない問題を修正
|
||||
* インスタンスブロックをしていてもRenote等すると取得されてしまう問題を修正
|
||||
* デザインの調整
|
||||
|
||||
10.93.1
|
||||
----------
|
||||
* データのエクスポートとインポートの動作を修正
|
||||
|
@ -46,6 +46,9 @@ Convert な(na) to にゃ(nya)
|
||||
Revert Nyaize
|
||||
|
||||
## Code style
|
||||
### Use semicolon
|
||||
To avoid ASI Hazard
|
||||
|
||||
### Don't use `export default`
|
||||
Bad:
|
||||
``` ts
|
||||
|
@ -304,6 +304,7 @@ common/views/pages/explore.vue:
|
||||
explore: "Prozkoumat {host}"
|
||||
common/views/components/url-preview.vue:
|
||||
enable-player: "Otevřít v přehrávači"
|
||||
disable-player: "Zavřít přehrávač"
|
||||
common/views/components/user-list.vue:
|
||||
no-users: "Žádní uživatelé"
|
||||
common/views/components/games/reversi/reversi.vue:
|
||||
@ -848,8 +849,6 @@ admin/views/dashboard.vue:
|
||||
instances: "Instance"
|
||||
this-instance: "Tato instance"
|
||||
federated: "Z fediversu"
|
||||
admin/views/queue.vue:
|
||||
operation: "Akce"
|
||||
admin/views/abuse.vue:
|
||||
details: "Popis"
|
||||
remove-report: "Odstranit"
|
||||
|
@ -169,9 +169,9 @@ common:
|
||||
deck-column-align-flexible: "Flexible"
|
||||
deck-column-width: "Deck column width"
|
||||
deck-column-width-narrow: "Narrow"
|
||||
deck-column-width-narrower: "Somewhat narrow"
|
||||
deck-column-width-narrower: "Narrower"
|
||||
deck-column-width-normal: "Regular"
|
||||
deck-column-width-wider: "Somewhat wide"
|
||||
deck-column-width-wider: "Slightly wide"
|
||||
deck-column-width-wide: "Wide"
|
||||
use-shadow: "Use shadows in the UI"
|
||||
rounded-corners: "Round the corners of the UI"
|
||||
@ -1057,7 +1057,7 @@ admin/views/dashboard.vue:
|
||||
this-instance: "This instance"
|
||||
federated: "Federated"
|
||||
admin/views/queue.vue:
|
||||
operation: "Action(s)"
|
||||
title: "Queue"
|
||||
remove-all-jobs: "Clear all queued jobs"
|
||||
admin/views/abuse.vue:
|
||||
title: "Abuse"
|
||||
|
@ -927,7 +927,6 @@ admin/views/dashboard.vue:
|
||||
this-instance: "Cette instance"
|
||||
federated: "Fédérées"
|
||||
admin/views/queue.vue:
|
||||
operation: "Action(s)"
|
||||
remove-all-jobs: "Enlever toutes les tâches en attente"
|
||||
admin/views/abuse.vue:
|
||||
title: "Abus"
|
||||
|
@ -14,6 +14,7 @@ const merge = (...args) => args.reduce((a, c) => ({
|
||||
}), {});
|
||||
|
||||
const languages = [
|
||||
'cs-CZ',
|
||||
'de-DE',
|
||||
'en-US',
|
||||
'es-ES',
|
||||
@ -24,9 +25,11 @@ const languages = [
|
||||
'nl-NL',
|
||||
'pl-PL',
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
];
|
||||
|
||||
const primaries = {
|
||||
'en': 'US',
|
||||
'ja': 'JP',
|
||||
'zh': 'CN',
|
||||
};
|
||||
|
@ -1408,12 +1408,13 @@ admin/views/hashtags.vue:
|
||||
hided-tags: "Hidden Tags"
|
||||
|
||||
admin/views/federation.vue:
|
||||
federation: "連合"
|
||||
instance: "インスタンス"
|
||||
host: "ホスト"
|
||||
notes: "投稿"
|
||||
users: "ユーザー"
|
||||
following: "フォロー中"
|
||||
followers: "フォロワー"
|
||||
caught-at: "登録日時"
|
||||
status: "ステータス"
|
||||
latest-request-sent-at: "直近のリクエスト送信"
|
||||
latest-request-received-at: "直近のリクエスト受信"
|
||||
@ -1422,7 +1423,7 @@ admin/views/federation.vue:
|
||||
block: "ブロック"
|
||||
marked-as-closed: "閉鎖されているとマーク"
|
||||
lookup: "照会"
|
||||
instances: "インスタンス"
|
||||
instances: "連合"
|
||||
instance-not-registered: "そのインスタンスは登録されていません"
|
||||
sort: "ソート"
|
||||
sorts:
|
||||
|
@ -860,8 +860,6 @@ admin/views/dashboard.vue:
|
||||
instances: "インスタンス"
|
||||
this-instance: "ワイのインスタンス"
|
||||
federated: "連合"
|
||||
admin/views/queue.vue:
|
||||
operation: "操作"
|
||||
admin/views/abuse.vue:
|
||||
details: "もっと"
|
||||
remove-report: "削除"
|
||||
|
@ -314,6 +314,7 @@ common/views/pages/explore.vue:
|
||||
users-info: "현재 {users} 사용자가 등록되어 있습니다"
|
||||
common/views/components/url-preview.vue:
|
||||
enable-player: "플레이어 열기"
|
||||
disable-player: "플레이어 닫기"
|
||||
common/views/components/user-list.vue:
|
||||
no-users: "사용자가 없습니다"
|
||||
common/views/components/games/reversi/reversi.vue:
|
||||
@ -1056,7 +1057,7 @@ admin/views/dashboard.vue:
|
||||
this-instance: "이 인스턴스"
|
||||
federated: "연합"
|
||||
admin/views/queue.vue:
|
||||
operation: "동작"
|
||||
title: "큐"
|
||||
remove-all-jobs: "모든 작업 제거"
|
||||
admin/views/abuse.vue:
|
||||
title: "스팸 신고"
|
||||
|
@ -121,12 +121,15 @@ common:
|
||||
other: "Inne"
|
||||
appearance: "Wygląd"
|
||||
behavior: "Zachowanie"
|
||||
fetch-on-scroll: "Automatycznie ładuj po przeciągnięciu w dół"
|
||||
note-visibility: "Widoczność wpisów"
|
||||
web-search-engine: "Wyszukiwarka internetowa"
|
||||
line-width-thin: "Cienka"
|
||||
line-width-normal: "Normalna"
|
||||
line-width-thick: "Gruba"
|
||||
font-size: "Rozmiar tekstu"
|
||||
font-size-medium: "Normalna"
|
||||
font-size-large: "Trochę duży"
|
||||
font-size-x-large: "Duży"
|
||||
deck-column-align-center: "Po środku"
|
||||
deck-column-align-left: "Z lewej"
|
||||
@ -137,6 +140,8 @@ common:
|
||||
deck-column-width-normal: "Normalna"
|
||||
deck-column-width-wider: "Trochę szerokie"
|
||||
deck-column-width-wide: "Szeroka"
|
||||
wallpaper: "Tapeta"
|
||||
choose-wallpaper: "Wybierz tapetę"
|
||||
timeline: "Oś czasu"
|
||||
navbar-position-left: "Z lewej"
|
||||
search: "Szukaj"
|
||||
|
@ -314,6 +314,7 @@ common/views/pages/explore.vue:
|
||||
users-info: "当前有{users}个注册用户"
|
||||
common/views/components/url-preview.vue:
|
||||
enable-player: "打开播放器"
|
||||
disable-player: "关闭播放器"
|
||||
common/views/components/user-list.vue:
|
||||
no-users: "无用户"
|
||||
common/views/components/games/reversi/reversi.vue:
|
||||
@ -1056,7 +1057,7 @@ admin/views/dashboard.vue:
|
||||
this-instance: "此实例"
|
||||
federated: "联合"
|
||||
admin/views/queue.vue:
|
||||
operation: "操作"
|
||||
title: "队列"
|
||||
remove-all-jobs: "清除所有作业"
|
||||
admin/views/abuse.vue:
|
||||
title: "举报垃圾信息"
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"author": "syuilo <i@syuilo.com>",
|
||||
"version": "10.93.1",
|
||||
"version": "10.96.0",
|
||||
"codename": "nighthike",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -181,7 +181,12 @@ export default Vue.extend({
|
||||
},
|
||||
grid: {
|
||||
clipMarkers: false,
|
||||
borderColor: 'rgba(0, 0, 0, 0.1)'
|
||||
borderColor: 'rgba(0, 0, 0, 0.1)',
|
||||
xaxis: {
|
||||
lines: {
|
||||
show: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
curve: 'straight',
|
@ -23,6 +23,8 @@ 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 {
|
||||
@ -124,7 +126,7 @@ export default Vue.extend({
|
||||
connection.on('statsLog', this.onStatsLog);
|
||||
connection.send('requestLog', {
|
||||
id: Math.random().toString().substr(2, 8),
|
||||
length: 100
|
||||
length: limit
|
||||
});
|
||||
|
||||
this.$once('hook:beforeDestroy', () => {
|
||||
@ -137,7 +139,7 @@ export default Vue.extend({
|
||||
methods: {
|
||||
onStats(stats) {
|
||||
this.stats.push(stats);
|
||||
if (this.stats.length > 100) this.stats.shift();
|
||||
if (this.stats.length > limit) this.stats.shift();
|
||||
},
|
||||
|
||||
onStatsLog(statsLog) {
|
||||
|
@ -92,8 +92,8 @@ import Vue from 'vue';
|
||||
import i18n from '../../i18n';
|
||||
import XCpuMemory from "./dashboard.cpu-memory.vue";
|
||||
import XQueue from "./dashboard.queue-charts.vue";
|
||||
import XCharts from "./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 MarqueeText from 'vue-marquee-text-component';
|
||||
import randomColor from 'randomcolor';
|
||||
|
@ -1,43 +1,58 @@
|
||||
<template>
|
||||
<div>
|
||||
<ui-card>
|
||||
<template #title><fa :icon="faTerminal"/> {{ $t('federation') }}</template>
|
||||
<template #title><fa :icon="faTerminal"/> {{ $t('instance') }}</template>
|
||||
<section class="fit-top">
|
||||
<ui-input class="target" v-model="target" type="text" @enter="showInstance()">
|
||||
<span>{{ $t('host') }}</span>
|
||||
<template #prefix><fa :icon="faServer"/></template>
|
||||
</ui-input>
|
||||
<ui-button @click="showInstance()"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button>
|
||||
|
||||
<div class="instance" v-if="instance">
|
||||
<ui-input :value="instance.host" type="text" readonly>
|
||||
<span>{{ $t('host') }}</span>
|
||||
</ui-input>
|
||||
<ui-horizon-group inputs>
|
||||
<ui-input :value="instance.host" type="text" readonly>
|
||||
<span>{{ $t('host') }}</span>
|
||||
<template #prefix><fa :icon="faServer"/></template>
|
||||
</ui-input>
|
||||
<ui-input :value="instance.caughtAt | date" type="text" readonly>
|
||||
<span>{{ $t('caught-at') }}</span>
|
||||
<template #prefix><fa :icon="faCrosshairs"/></template>
|
||||
</ui-input>
|
||||
</ui-horizon-group>
|
||||
<ui-horizon-group inputs>
|
||||
<ui-input :value="instance.notesCount | number" type="text" readonly>
|
||||
<span>{{ $t('notes') }}</span>
|
||||
<template #prefix><fa :icon="faEnvelopeOpenText"/></template>
|
||||
</ui-input>
|
||||
<ui-input :value="instance.usersCount | number" type="text" readonly>
|
||||
<span>{{ $t('users') }}</span>
|
||||
<template #prefix><fa :icon="faUsers"/></template>
|
||||
</ui-input>
|
||||
</ui-horizon-group>
|
||||
<ui-horizon-group inputs>
|
||||
<ui-input :value="instance.followingCount | number" type="text" readonly>
|
||||
<span>{{ $t('following') }}</span>
|
||||
<template #prefix><fa :icon="faCaretDown"/></template>
|
||||
</ui-input>
|
||||
<ui-input :value="instance.followersCount | number" type="text" readonly>
|
||||
<span>{{ $t('followers') }}</span>
|
||||
<template #prefix><fa :icon="faCaretUp"/></template>
|
||||
</ui-input>
|
||||
</ui-horizon-group>
|
||||
<ui-horizon-group inputs>
|
||||
<ui-input :value="instance.latestRequestSentAt" type="text" readonly>
|
||||
<ui-input :value="instance.latestRequestSentAt | date" type="text" readonly>
|
||||
<span>{{ $t('latest-request-sent-at') }}</span>
|
||||
<template #prefix><fa :icon="faPaperPlane"/></template>
|
||||
</ui-input>
|
||||
<ui-input :value="instance.latestStatus" type="text" readonly>
|
||||
<span>{{ $t('status') }}</span>
|
||||
<template #prefix><fa :icon="faTrafficLight"/></template>
|
||||
</ui-input>
|
||||
</ui-horizon-group>
|
||||
<ui-input :value="instance.latestRequestReceivedAt" type="text" readonly>
|
||||
<ui-input :value="instance.latestRequestReceivedAt | date" type="text" readonly>
|
||||
<span>{{ $t('latest-request-received-at') }}</span>
|
||||
<template #prefix><fa :icon="faInbox"/></template>
|
||||
</ui-input>
|
||||
<ui-switch v-model="instance.isBlocked" @change="updateInstance()">{{ $t('block') }}</ui-switch>
|
||||
<ui-switch v-model="instance.isMarkedAsClosed" @change="updateInstance()">{{ $t('marked-as-closed') }}</ui-switch>
|
||||
@ -133,7 +148,8 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../i18n';
|
||||
import { faGlobe, faTerminal, faSearch, faMinusCircle, faServer } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faPaperPlane } from '@fortawesome/free-regular-svg-icons';
|
||||
import { faGlobe, faTerminal, faSearch, faMinusCircle, faServer, faCrosshairs, faEnvelopeOpenText, faUsers, faCaretDown, faCaretUp, faTrafficLight, faInbox } from '@fortawesome/free-solid-svg-icons';
|
||||
import ApexCharts from 'apexcharts';
|
||||
import * as tinycolor from 'tinycolor2';
|
||||
|
||||
@ -144,19 +160,23 @@ const negate = arr => arr.map(x => -x);
|
||||
export default Vue.extend({
|
||||
i18n: i18n('admin/views/federation.vue'),
|
||||
|
||||
filters: {
|
||||
date: v => v ? new Date(v).toLocaleString() : 'N/A'
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
instance: null,
|
||||
target: null,
|
||||
sort: '+lastCommunicatedAt',
|
||||
state: 'all',
|
||||
limit: 50,
|
||||
limit: 100,
|
||||
instances: [],
|
||||
chart: null,
|
||||
chartSrc: 'requests',
|
||||
chartSpan: 'hour',
|
||||
chartInstance: null,
|
||||
faGlobe, faTerminal, faSearch, faMinusCircle, faServer
|
||||
faGlobe, faTerminal, faSearch, faMinusCircle, faServer, faCrosshairs, faEnvelopeOpenText, faUsers, faCaretDown, faCaretUp, faPaperPlane, faTrafficLight, faInbox
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -6,6 +6,7 @@
|
||||
<ui-input :value="host" readonly>{{ $t('host') }}</ui-input>
|
||||
<ui-input v-model="name">{{ $t('instance-name') }}</ui-input>
|
||||
<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="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>
|
||||
@ -157,6 +158,7 @@ export default Vue.extend({
|
||||
mascotImageUrl: null,
|
||||
bannerUrl: null,
|
||||
errorImageUrl: null,
|
||||
iconUrl: null,
|
||||
name: null,
|
||||
description: null,
|
||||
languages: null,
|
||||
@ -207,6 +209,7 @@ export default Vue.extend({
|
||||
this.mascotImageUrl = meta.mascotImageUrl;
|
||||
this.bannerUrl = meta.bannerUrl;
|
||||
this.errorImageUrl = meta.errorImageUrl;
|
||||
this.iconUrl = meta.iconUrl;
|
||||
this.name = meta.name;
|
||||
this.description = meta.description;
|
||||
this.languages = meta.langs.join(' ');
|
||||
@ -267,6 +270,7 @@ export default Vue.extend({
|
||||
mascotImageUrl: this.mascotImageUrl,
|
||||
bannerUrl: this.bannerUrl,
|
||||
errorImageUrl: this.errorImageUrl,
|
||||
iconUrl: this.iconUrl,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
langs: this.languages.split(' '),
|
||||
|
@ -4,7 +4,7 @@
|
||||
<template #title><fa :icon="faStream"/> {{ $t('logs') }}</template>
|
||||
<section class="fit-top">
|
||||
<ui-horizon-group inputs>
|
||||
<ui-input v-model="domain" debounce>
|
||||
<ui-input v-model="domain" :debounce="true">
|
||||
<span>{{ $t('domain') }}</span>
|
||||
</ui-input>
|
||||
<ui-select v-model="level">
|
||||
|
@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<ui-card>
|
||||
<template #title><fa :icon="faTasks"/> {{ $t('title') }}</template>
|
||||
<template #title><fa :icon="faChartBar"/> {{ $t('title') }}</template>
|
||||
<section class="wptihjuy">
|
||||
<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-input :value="latestStats.deliver.activeSincePrevTick | number" type="text" readonly>
|
||||
<span>Process</span>
|
||||
<template #prefix><fa :icon="fasPlayCircle"/></template>
|
||||
<template #suffix>jobs/s</template>
|
||||
<template #suffix>jobs/tick</template>
|
||||
</ui-input>
|
||||
<ui-input :value="latestStats.deliver.active | number" type="text" readonly>
|
||||
<span>Active</span>
|
||||
@ -30,11 +31,12 @@
|
||||
</section>
|
||||
<section class="wptihjuy">
|
||||
<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-input :value="latestStats.inbox.activeSincePrevTick | number" type="text" readonly>
|
||||
<span>Process</span>
|
||||
<template #prefix><fa :icon="fasPlayCircle"/></template>
|
||||
<template #suffix>jobs/s</template>
|
||||
<template #suffix>jobs/tick</template>
|
||||
</ui-input>
|
||||
<ui-input :value="latestStats.inbox.active | number" type="text" readonly>
|
||||
<span>Active</span>
|
||||
@ -58,6 +60,35 @@
|
||||
<ui-button @click="removeAllJobs">{{ $t('remove-all-jobs') }}</ui-button>
|
||||
</section>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@ -67,7 +98,9 @@ import i18n from '../../i18n';
|
||||
import ApexCharts from 'apexcharts';
|
||||
import * as tinycolor from 'tinycolor2';
|
||||
import { faTasks, faInbox, faStopwatch, faPlayCircle as fasPlayCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faPaperPlane, faStopCircle, faPlayCircle as farPlayCircle } from '@fortawesome/free-regular-svg-icons';
|
||||
import { faPaperPlane, faStopCircle, faPlayCircle as farPlayCircle, faChartBar } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
const limit = 200;
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('admin/views/queue.vue'),
|
||||
@ -77,7 +110,11 @@ export default Vue.extend({
|
||||
stats: [],
|
||||
deliverChart: null,
|
||||
inboxChart: null,
|
||||
faTasks, faPaperPlane, faInbox, faStopwatch, faStopCircle, farPlayCircle, fasPlayCircle
|
||||
jobs: [],
|
||||
jobsLimit: 50,
|
||||
domain: 'deliver',
|
||||
state: 'delayed',
|
||||
faTasks, faPaperPlane, faInbox, faStopwatch, faStopCircle, farPlayCircle, fasPlayCircle, faChartBar
|
||||
};
|
||||
},
|
||||
|
||||
@ -91,36 +128,58 @@ export default Vue.extend({
|
||||
stats(stats) {
|
||||
this.inboxChart.updateSeries([{
|
||||
name: 'Process',
|
||||
type: 'area',
|
||||
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',
|
||||
type: 'line',
|
||||
data: stats.map((x, i) => ({ x: i, y: x.inbox.waiting }))
|
||||
}, {
|
||||
name: 'Delayed',
|
||||
type: 'line',
|
||||
data: stats.map((x, i) => ({ x: i, y: x.inbox.delayed }))
|
||||
}]);
|
||||
this.deliverChart.updateSeries([{
|
||||
name: 'Process',
|
||||
type: 'area',
|
||||
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',
|
||||
type: 'line',
|
||||
data: stats.map((x, i) => ({ x: i, y: x.deliver.waiting }))
|
||||
}, {
|
||||
name: 'Delayed',
|
||||
type: 'line',
|
||||
data: stats.map((x, i) => ({ x: i, y: x.deliver.delayed }))
|
||||
}]);
|
||||
}
|
||||
},
|
||||
|
||||
domain() {
|
||||
this.jobs = [];
|
||||
this.fetchJobs();
|
||||
},
|
||||
|
||||
state() {
|
||||
this.jobs = [];
|
||||
this.fetchJobs();
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const chartOpts = {
|
||||
this.fetchJobs();
|
||||
|
||||
const chartOpts = id => ({
|
||||
chart: {
|
||||
id,
|
||||
group: 'queue',
|
||||
type: 'area',
|
||||
height: 200,
|
||||
animations: {
|
||||
@ -140,7 +199,12 @@ export default Vue.extend({
|
||||
},
|
||||
grid: {
|
||||
clipMarkers: false,
|
||||
borderColor: 'rgba(0, 0, 0, 0.1)'
|
||||
borderColor: 'rgba(0, 0, 0, 0.1)',
|
||||
xaxis: {
|
||||
lines: {
|
||||
show: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
curve: 'straight',
|
||||
@ -169,10 +233,10 @@ export default Vue.extend({
|
||||
show: false,
|
||||
min: 0,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
this.inboxChart = new ApexCharts(this.$refs.inboxChart, chartOpts);
|
||||
this.deliverChart = new ApexCharts(this.$refs.deliverChart, chartOpts);
|
||||
this.inboxChart = new ApexCharts(this.$refs.inboxChart, chartOpts('a'));
|
||||
this.deliverChart = new ApexCharts(this.$refs.deliverChart, chartOpts('b'));
|
||||
|
||||
this.inboxChart.render();
|
||||
this.deliverChart.render();
|
||||
@ -182,7 +246,7 @@ export default Vue.extend({
|
||||
connection.on('statsLog', this.onStatsLog);
|
||||
connection.send('requestLog', {
|
||||
id: Math.random().toString().substr(2, 8),
|
||||
length: 100
|
||||
length: limit
|
||||
});
|
||||
|
||||
this.$once('hook:beforeDestroy', () => {
|
||||
@ -212,14 +276,24 @@ export default Vue.extend({
|
||||
|
||||
onStats(stats) {
|
||||
this.stats.push(stats);
|
||||
if (this.stats.length > 100) this.stats.shift();
|
||||
if (this.stats.length > limit) this.stats.shift();
|
||||
},
|
||||
|
||||
onStatsLog(statsLog) {
|
||||
for (const stats of statsLog.reverse()) {
|
||||
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>
|
||||
@ -228,5 +302,10 @@ export default Vue.extend({
|
||||
.wptihjuy
|
||||
> .chart
|
||||
min-height 200px !important
|
||||
margin 0 -8px
|
||||
|
||||
.xvvuvgsv
|
||||
> b
|
||||
margin-right 16px
|
||||
|
||||
</style>
|
||||
|
@ -69,7 +69,7 @@ export default Vue.extend({
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: '90%'
|
||||
columnWidth: '80%'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
|
@ -93,12 +93,17 @@ export default Vue.extend({
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: '90%'
|
||||
columnWidth: '80%'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
clipMarkers: false,
|
||||
borderColor: 'rgba(0, 0, 0, 0.1)'
|
||||
borderColor: 'rgba(0, 0, 0, 0.1)',
|
||||
xaxis: {
|
||||
lines: {
|
||||
show: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
shared: true,
|
||||
|
@ -366,9 +366,6 @@ root(fill)
|
||||
&[type='file']
|
||||
display none
|
||||
|
||||
&[type='number']
|
||||
text-align right
|
||||
|
||||
> .prefix
|
||||
> .suffix
|
||||
display block
|
||||
|
@ -172,7 +172,7 @@ export default Vue.extend({
|
||||
},
|
||||
plotOptions: {
|
||||
bar: {
|
||||
columnWidth: '90%'
|
||||
columnWidth: '80%'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
|
@ -28,10 +28,10 @@ export default Vue.extend({
|
||||
computed: {
|
||||
template(): string {
|
||||
let t = '';
|
||||
if (this.title && this.url) t += `【[${title}](${url})】\n`;
|
||||
if (this.title && !this.url) t += `【${title}】\n`;
|
||||
if (this.text) t += `${text}\n`;
|
||||
if (!this.title && this.url) t += `${url}`;
|
||||
if (this.title && this.url) t += `【[${this.title}](${this.url})】\n`;
|
||||
if (this.title && !this.url) t += `【${this.title}】\n`;
|
||||
if (this.text) t += `${this.text}\n`;
|
||||
if (!this.title && this.url) t += `${this.url}`;
|
||||
return t.trim();
|
||||
}
|
||||
},
|
||||
|
@ -42,21 +42,29 @@ export default define({
|
||||
watch: {
|
||||
stats(stats) {
|
||||
this.inChart.updateSeries([{
|
||||
type: 'area',
|
||||
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 }))
|
||||
}]);
|
||||
this.outChart.updateSeries([{
|
||||
type: 'area',
|
||||
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 }))
|
||||
}]);
|
||||
}
|
||||
|
@ -480,7 +480,7 @@ export default Vue.extend({
|
||||
});
|
||||
|
||||
if (this.text && this.text != '') {
|
||||
const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag);
|
||||
const hashtags = parse(this.text).filter(x => x.node.type === 'hashtag').map(x => x.node.props.hashtag);
|
||||
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
|
||||
localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
|
||||
}
|
||||
|
@ -367,7 +367,7 @@ export default Vue.extend({
|
||||
});
|
||||
|
||||
if (this.text && this.text != '') {
|
||||
const hashtags = parse(this.text).filter(x => x.type == 'hashtag').map(x => x.hashtag);
|
||||
const hashtags = parse(this.text).filter(x => x.node.type === 'hashtag').map(x => x.node.props.hashtag);
|
||||
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
|
||||
localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { deliverQueue, inboxQueue } from '../queue';
|
||||
|
||||
const ev = new Xev();
|
||||
|
||||
const interval = 1000;
|
||||
const interval = 3000;
|
||||
|
||||
/**
|
||||
* Report queue stats regularly
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { parseFragment, DefaultTreeDocumentFragment } from 'parse5';
|
||||
import { URL } from 'url';
|
||||
import { urlRegex } from './prelude';
|
||||
|
||||
export function fromHtml(html: string): string {
|
||||
if (html == null) return null;
|
||||
@ -14,7 +15,7 @@ export function fromHtml(html: string): string {
|
||||
|
||||
return text.trim();
|
||||
|
||||
function getText(node: any) {
|
||||
function getText(node: any): string {
|
||||
if (node.nodeName == '#text') return node.value;
|
||||
|
||||
if (node.childNodes) {
|
||||
@ -38,10 +39,11 @@ export function fromHtml(html: string): string {
|
||||
const txt = getText(node);
|
||||
const rel = node.attrs.find((x: any) => x.name == 'rel');
|
||||
const href = node.attrs.find((x: any) => x.name == 'href');
|
||||
const isHashtag = rel && rel.value.match('tag') !== null;
|
||||
|
||||
// ハッシュタグ / hrefがない / txtがURL
|
||||
if ((rel && rel.value.match('tag') !== null) || !href || href.value == txt) {
|
||||
text += txt;
|
||||
if (isHashtag || !href || href.value == txt) {
|
||||
text += isHashtag || txt.match(urlRegex) ? txt : `<${txt}>`;
|
||||
// メンション
|
||||
} else if (txt.startsWith('@') && !(rel && rel.value.match(/^me /))) {
|
||||
const part = txt.split('@');
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as P from 'parsimmon';
|
||||
import { createLeaf, createTree } from './types';
|
||||
import { createLeaf, createTree, urlRegex } from './prelude';
|
||||
import { takeWhile, cumulativeSum } from '../prelude/array';
|
||||
import parseAcct from '../misc/acct/parse';
|
||||
import { toUnicode } from 'punycode';
|
||||
@ -154,9 +154,16 @@ export const mfmLanguage = P.createLanguage({
|
||||
url: () => {
|
||||
return P((input, i) => {
|
||||
const text = input.substr(i);
|
||||
const match = text.match(/^https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.,=\+\-]+/);
|
||||
if (!match) return P.makeFailure(i, 'not a url');
|
||||
let url = match[0];
|
||||
const match = text.match(urlRegex);
|
||||
let url: string;
|
||||
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);
|
||||
if (url.endsWith('.')) url = url.substr(0, url.lastIndexOf('.'));
|
||||
if (url.endsWith(',')) url = url.substr(0, url.lastIndexOf(','));
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as A from '../prelude/array';
|
||||
import * as S from '../prelude/string';
|
||||
import { MfmForest, MfmTree } from './types';
|
||||
import { MfmForest, MfmTree } from './prelude';
|
||||
import { createTree, createLeaf } from '../prelude/tree';
|
||||
|
||||
function isEmptyTextTree(t: MfmTree): boolean {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { mfmLanguage } from './language';
|
||||
import { MfmForest } from './types';
|
||||
import { MfmForest } from './prelude';
|
||||
import { normalize } from './normalize';
|
||||
|
||||
export function parse(source: string): MfmForest {
|
||||
|
@ -35,3 +35,5 @@ export function createLeaf(type: string, props: any): MfmTree {
|
||||
export function createTree(type: string, children: MfmForest, props: any): MfmTree {
|
||||
return T.createTree({ type, props }, children);
|
||||
}
|
||||
|
||||
export const urlRegex = /^https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.,=\+\-]+/;
|
@ -2,7 +2,7 @@ import { JSDOM } from 'jsdom';
|
||||
import config from '../config';
|
||||
import { INote } from '../models/note';
|
||||
import { intersperse } from '../prelude/array';
|
||||
import { MfmForest, MfmTree } from './types';
|
||||
import { MfmForest, MfmTree } from './prelude';
|
||||
|
||||
export function toHtml(tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteUsers'] = []) {
|
||||
if (tokens == null) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
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)}`;
|
||||
@ -10,6 +11,11 @@ export function isSelfHost(host: string) {
|
||||
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());
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { EmojiNode, MfmForest } from '../mfm/types';
|
||||
import { EmojiNode, MfmForest } from '../mfm/prelude';
|
||||
import { preorderF } from '../prelude/tree';
|
||||
import { unique } from '../prelude/array';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { HashtagNode, MfmForest } from '../mfm/types';
|
||||
import { HashtagNode, MfmForest } from '../mfm/prelude';
|
||||
import { preorderF } from '../prelude/tree';
|
||||
import { unique } from '../prelude/array';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
// test is located in test/extract-mentions
|
||||
|
||||
import { MentionNode, MfmForest } from '../mfm/types';
|
||||
import { MentionNode, MfmForest } from '../mfm/prelude';
|
||||
import { preorderF } from '../prelude/tree';
|
||||
|
||||
export default function(mfmForest: MfmForest): MentionNode['props'][] {
|
||||
|
@ -198,6 +198,7 @@ export type IMeta = {
|
||||
mascotImageUrl?: string;
|
||||
bannerUrl?: string;
|
||||
errorImageUrl?: string;
|
||||
iconUrl?: string;
|
||||
|
||||
cacheRemoteFiles?: boolean;
|
||||
|
||||
|
@ -41,6 +41,7 @@ export type INote = {
|
||||
replyId: mongo.ObjectID;
|
||||
renoteId: mongo.ObjectID;
|
||||
poll: IPoll;
|
||||
name?: string;
|
||||
text: string;
|
||||
tags: string[];
|
||||
tagsLower: string[];
|
||||
@ -391,6 +392,10 @@ export const pack = async (
|
||||
}
|
||||
//#endregion
|
||||
|
||||
if (_note.name) {
|
||||
_note.text = `【${_note.name}】\n${_note.text}`;
|
||||
}
|
||||
|
||||
if (_note.user.isCat && _note.text) {
|
||||
_note.text = (_note.text
|
||||
// ja-JP
|
||||
|
@ -20,6 +20,7 @@ User.createIndex('createdAt');
|
||||
User.createIndex('updatedAt');
|
||||
User.createIndex('followersCount');
|
||||
User.createIndex('tags');
|
||||
User.createIndex('isSuspended');
|
||||
User.createIndex('username');
|
||||
User.createIndex('usernameLower');
|
||||
User.createIndex('host');
|
||||
|
@ -178,10 +178,10 @@ export function destroy() {
|
||||
deliverQueue.once('cleaned', (jobs, status) => {
|
||||
deliverLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
||||
});
|
||||
deliverQueue.clean(0, 'wait');
|
||||
deliverQueue.clean(0, 'delayed');
|
||||
|
||||
inboxQueue.once('cleaned', (jobs, status) => {
|
||||
inboxLogger.succ(`Cleaned ${jobs.length} ${status} jobs`);
|
||||
});
|
||||
inboxQueue.clean(0, 'wait');
|
||||
inboxQueue.clean(0, 'delayed');
|
||||
}
|
||||
|
@ -24,10 +24,8 @@ export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> =>
|
||||
|
||||
switch (object.type) {
|
||||
case 'Note':
|
||||
announceNote(resolver, actor, activity, object as INote);
|
||||
break;
|
||||
|
||||
case 'Question':
|
||||
case 'Article':
|
||||
announceNote(resolver, actor, activity, object as INote);
|
||||
break;
|
||||
|
||||
|
@ -5,6 +5,8 @@ import { IAnnounce, INote } from '../../type';
|
||||
import { fetchNote, resolveNote } from '../../models/note';
|
||||
import { resolvePerson } from '../../models/person';
|
||||
import { apLogger } from '../../logger';
|
||||
import { extractDbHost } from '../../../../misc/convert-host';
|
||||
import Instance from '../../../../models/instance';
|
||||
|
||||
const logger = apLogger;
|
||||
|
||||
@ -23,6 +25,11 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
|
||||
throw new Error('invalid announce');
|
||||
}
|
||||
|
||||
// アナウンス先をブロックしてたら中断
|
||||
// TODO: いちいちデータベースにアクセスするのはコスト高そうなのでどっかにキャッシュしておく
|
||||
const instance = await Instance.findOne({ host: extractDbHost(uri) });
|
||||
if (instance && instance.isBlocked) return;
|
||||
|
||||
// 既に同じURIを持つものが登録されていないかチェック
|
||||
const exist = await fetchNote(uri);
|
||||
if (exist) {
|
||||
|
@ -29,10 +29,8 @@ export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
|
||||
break;
|
||||
|
||||
case 'Note':
|
||||
createNote(resolver, actor, object);
|
||||
break;
|
||||
|
||||
case 'Question':
|
||||
case 'Article':
|
||||
createNote(resolver, actor, object);
|
||||
break;
|
||||
|
||||
|
@ -21,10 +21,8 @@ export default async (actor: IRemoteUser, activity: IDelete): Promise<void> => {
|
||||
|
||||
switch (object.type) {
|
||||
case 'Note':
|
||||
deleteNote(actor, uri);
|
||||
break;
|
||||
|
||||
case 'Question':
|
||||
case 'Article':
|
||||
deleteNote(actor, uri);
|
||||
break;
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
import { remoteLogger } from "../logger";
|
||||
import { remoteLogger } from '../logger';
|
||||
|
||||
export const apLogger = remoteLogger.createSubLogger('ap', 'magenta');
|
||||
|
@ -19,6 +19,8 @@ import vote from '../../../services/note/polls/vote';
|
||||
import { apLogger } from '../logger';
|
||||
import { IDriveFile } from '../../../models/drive-file';
|
||||
import { deliverQuestionUpdate } from '../../../services/note/polls/update';
|
||||
import Instance from '../../../models/instance';
|
||||
import { extractDbHost } from '../../../misc/convert-host';
|
||||
|
||||
const logger = apLogger;
|
||||
|
||||
@ -55,7 +57,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
|
||||
|
||||
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}`, {
|
||||
resolver: {
|
||||
history: resolver.getHistory()
|
||||
@ -132,7 +134,15 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
|
||||
let quote: INote;
|
||||
|
||||
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;
|
||||
@ -189,6 +199,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
|
||||
files,
|
||||
reply,
|
||||
renote: quote,
|
||||
name: note.name,
|
||||
cw,
|
||||
text,
|
||||
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> {
|
||||
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 このサーバーに既に登録されていたらそれを返す
|
||||
const exist = await fetchNote(uri);
|
||||
|
||||
|
@ -294,6 +294,13 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
|
||||
await User.update({ _id: exist._id }, {
|
||||
$set: {
|
||||
lastFetchedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
if (resolver == null) resolver = new Resolver();
|
||||
|
||||
const object = hint || await resolver.resolve(uri) as any;
|
||||
|
@ -1,3 +1,3 @@
|
||||
import Logger from "../services/logger";
|
||||
import Logger from '../services/logger';
|
||||
|
||||
export const remoteLogger = new Logger('remote', 'cyan');
|
||||
|
@ -17,6 +17,7 @@ import Following from './activitypub/following';
|
||||
import Featured from './activitypub/featured';
|
||||
import renderQuestion from '../remote/activitypub/renderer/question';
|
||||
import { inbox as processInbox } from '../queue';
|
||||
import { isSelfHost } from '../misc/convert-host';
|
||||
|
||||
// Init router
|
||||
const router = new Router();
|
||||
@ -79,6 +80,16 @@ router.get('/notes/:note', async (ctx, next) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// リモートだったらリダイレクト
|
||||
if (note._user.host != null) {
|
||||
if (note.uri == null || isSelfHost(note._user.host)) {
|
||||
ctx.status = 500;
|
||||
return;
|
||||
}
|
||||
ctx.redirect(note.uri);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.body = renderActivity(await renderNote(note, false));
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
setResponseType(ctx);
|
||||
@ -93,6 +104,7 @@ router.get('/notes/:note/activity', async ctx => {
|
||||
|
||||
const note = await Note.findOne({
|
||||
_id: new ObjectID(ctx.params.note),
|
||||
'_user.host': null,
|
||||
visibility: { $in: ['public', 'home'] },
|
||||
localOnly: { $ne: true }
|
||||
});
|
||||
@ -116,6 +128,7 @@ router.get('/questions/:question', async (ctx, next) => {
|
||||
|
||||
const poll = await Note.findOne({
|
||||
_id: new ObjectID(ctx.params.question),
|
||||
'_user.host': null,
|
||||
visibility: { $in: ['public', 'home'] },
|
||||
localOnly: { $ne: true },
|
||||
poll: {
|
||||
|
40
src/server/api/endpoints/admin/queue/jobs.ts
Normal file
40
src/server/api/endpoints/admin/queue/jobs.ts
Normal 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,
|
||||
}));
|
||||
});
|
@ -1,7 +1,9 @@
|
||||
import $ from 'cafy';
|
||||
import ID, { transform } from '../../../../misc/cafy-id';
|
||||
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 = {
|
||||
desc: {
|
||||
@ -51,5 +53,25 @@ export default define(meta, async (ps) => {
|
||||
}
|
||||
});
|
||||
|
||||
unFollowAll(user);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -69,6 +69,13 @@ export const meta = {
|
||||
}
|
||||
},
|
||||
|
||||
iconUrl: {
|
||||
validator: $.optional.nullable.str,
|
||||
desc: {
|
||||
'ja-JP': 'インスタンスのアイコンURL'
|
||||
}
|
||||
},
|
||||
|
||||
name: {
|
||||
validator: $.optional.nullable.str,
|
||||
desc: {
|
||||
@ -356,6 +363,10 @@ export default define(meta, async (ps) => {
|
||||
set.bannerUrl = ps.bannerUrl;
|
||||
}
|
||||
|
||||
if (ps.iconUrl !== undefined) {
|
||||
set.iconUrl = ps.iconUrl;
|
||||
}
|
||||
|
||||
if (ps.name !== undefined) {
|
||||
set.name = ps.name;
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ import Note, { pack as packNote, INote } from '../../../../models/note';
|
||||
import { createNote } from '../../../../remote/activitypub/models/note';
|
||||
import Resolver from '../../../../remote/activitypub/resolver';
|
||||
import { ApiError } from '../../error';
|
||||
import Instance from '../../../../models/instance';
|
||||
import { extractDbHost } from '../../../../misc/convert-host';
|
||||
|
||||
export const meta = {
|
||||
tags: ['federation'],
|
||||
@ -61,6 +63,10 @@ async function fetchAny(uri: string) {
|
||||
if (packed !== null) return packed;
|
||||
}
|
||||
|
||||
// ブロックしてたら中断
|
||||
const instance = await Instance.findOne({ host: extractDbHost(uri) });
|
||||
if (instance && instance.isBlocked) return null;
|
||||
|
||||
// URI(AP Object id)としてDB検索
|
||||
{
|
||||
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);
|
||||
return {
|
||||
type: 'Note',
|
||||
|
@ -116,6 +116,7 @@ export default define(meta, async (ps, me) => {
|
||||
mascotImageUrl: instance.mascotImageUrl,
|
||||
bannerUrl: instance.bannerUrl,
|
||||
errorImageUrl: instance.errorImageUrl,
|
||||
iconUrl: instance.iconUrl,
|
||||
maxNoteTextLength: instance.maxNoteTextLength,
|
||||
emojis: emojis,
|
||||
enableEmail: instance.enableEmail,
|
||||
|
@ -70,7 +70,8 @@ export default define(meta, async (ps, me) => {
|
||||
users = await User
|
||||
.find({
|
||||
host: null,
|
||||
usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase()))
|
||||
usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase())),
|
||||
isSuspended: { $ne: true }
|
||||
}, {
|
||||
limit: ps.limit,
|
||||
skip: ps.offset
|
||||
@ -80,7 +81,8 @@ export default define(meta, async (ps, me) => {
|
||||
const otherUsers = await User
|
||||
.find({
|
||||
host: { $ne: null },
|
||||
usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase()))
|
||||
usernameLower: new RegExp('^' + escapeRegexp(ps.query.replace('@', '').toLowerCase())),
|
||||
isSuspended: { $ne: true }
|
||||
}, {
|
||||
limit: ps.limit - users.length
|
||||
});
|
||||
|
@ -1,3 +1,3 @@
|
||||
import Logger from "../../services/logger";
|
||||
import Logger from '../../services/logger';
|
||||
|
||||
export const apiLogger = new Logger('api');
|
||||
|
@ -250,7 +250,10 @@ router.get('/reversi', async ctx => ctx.redirect(override(ctx.URL.pathname, 'gam
|
||||
router.get('*', async ctx => {
|
||||
const meta = await fetchMeta();
|
||||
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');
|
||||
});
|
||||
|
@ -8,17 +8,19 @@ html
|
||||
|
||||
head
|
||||
meta(charset='utf-8')
|
||||
meta(name='application-name' content='Misskey')
|
||||
meta(name='application-name' content= title || 'Misskey')
|
||||
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')
|
||||
|
||||
title
|
||||
block title
|
||||
| Misskey
|
||||
= title || 'Misskey'
|
||||
|
||||
block desc
|
||||
meta(name='description' content='✨🌎✨ A federated blogging platform ✨🚀✨')
|
||||
meta(name='description' content= desc || '✨🌎✨ A federated blogging platform ✨🚀✨')
|
||||
|
||||
block meta
|
||||
|
||||
|
@ -31,3 +31,5 @@ block meta
|
||||
|
||||
if !user.host
|
||||
link(rel='alternate' href=url type='application/activity+json')
|
||||
if note.uri
|
||||
link(rel='alternate' href=note.uri type='application/activity+json')
|
||||
|
@ -1,3 +1,3 @@
|
||||
import Logger from "../logger";
|
||||
import Logger from '../logger';
|
||||
|
||||
export const driveLogger = new Logger('drive', 'blue');
|
||||
|
@ -13,7 +13,7 @@ import instanceChart from '../../services/chart/instance';
|
||||
|
||||
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({
|
||||
followerId: follower._id,
|
||||
followeeId: followee._id
|
||||
@ -71,7 +71,7 @@ export default async function(follower: IUser, followee: IUser) {
|
||||
perUserFollowingChart.update(follower, followee, false);
|
||||
|
||||
// Publish unfollow event
|
||||
if (isLocalUser(follower)) {
|
||||
if (!silent && isLocalUser(follower)) {
|
||||
packUser(followee, follower, {
|
||||
detail: true
|
||||
}).then(packed => publishMainStream(follower._id, 'unfollow', packed));
|
||||
|
@ -91,6 +91,7 @@ class NotificationManager {
|
||||
|
||||
type Option = {
|
||||
createdAt?: Date;
|
||||
name?: string;
|
||||
text?: string;
|
||||
reply?: 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) : [],
|
||||
replyId: data.reply ? data.reply._id : null,
|
||||
renoteId: data.renote ? data.renote._id : null,
|
||||
name: data.name,
|
||||
text: data.text,
|
||||
poll: data.poll,
|
||||
cw: data.cw == null ? null : data.cw,
|
||||
|
16
test/mfm.ts
16
test/mfm.ts
@ -12,7 +12,7 @@ import * as assert from 'assert';
|
||||
|
||||
import { parse, parsePlain } from '../src/mfm/parse';
|
||||
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';
|
||||
|
||||
function text(text: string): MfmTree {
|
||||
@ -840,6 +840,20 @@ describe('MFM', () => {
|
||||
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', () => {
|
||||
|
Reference in New Issue
Block a user