Merge branch 'develop' into sw-notification-action
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<MkModal ref="modal" :manual-showing="manualShowing" :src="src" @click="$refs.modal.close()" @opening="opening" @close="$emit('close')" @closed="$emit('closed')">
|
||||
<MkModal ref="modal" :manual-showing="manualShowing" :src="src" :front="true" @click="$refs.modal.close()" @opening="opening" @close="$emit('close')" @closed="$emit('closed')">
|
||||
<MkEmojiPicker :show-pinned="showPinned" :as-reaction-picker="asReactionPicker" @chosen="chosen" ref="picker"/>
|
||||
</MkModal>
|
||||
</template>
|
||||
|
@@ -9,8 +9,9 @@
|
||||
<div class="menu" v-else>
|
||||
<div class="body">
|
||||
<div>Ads by {{ host }}</div>
|
||||
<!--<MkButton>{{ $ts.stopThisAd }}</MkButton>-->
|
||||
<button class="_textButton" @click="toggleMenu">{{ $ts.close }}</button>
|
||||
<!--<MkButton class="button" primary>{{ $ts._ad.like }}</MkButton>-->
|
||||
<MkButton v-if="ad.ratio !== 0" class="button" @click="reduceFrequency">{{ $ts._ad.reduceFrequencyOfThisAd }}</MkButton>
|
||||
<button class="_textButton" @click="toggleMenu">{{ $ts._ad.back }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -19,9 +20,11 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import { instance } from '@client/instance';
|
||||
import { Instance, instance } from '@client/instance';
|
||||
import { host } from '@client/config';
|
||||
import MkButton from '@client/components/ui/button.vue';
|
||||
import { defaultStore } from '@client/store';
|
||||
import * as os from '@client/os';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@@ -45,35 +48,65 @@ export default defineComponent({
|
||||
showMenu.value = !showMenu.value;
|
||||
};
|
||||
|
||||
let ad = null;
|
||||
const choseAd = (): Instance['ads'][number] | null => {
|
||||
if (props.specify) {
|
||||
return props.specify as Instance['ads'][number];
|
||||
}
|
||||
|
||||
if (props.specify) {
|
||||
ad = props.specify;
|
||||
} else {
|
||||
let ads = instance.ads.filter(ad => props.prefer.includes(ad.place));
|
||||
const allAds = instance.ads.map(ad => defaultStore.state.mutedAds.includes(ad.id) ? {
|
||||
...ad,
|
||||
ratio: 0
|
||||
} : ad);
|
||||
|
||||
let ads = allAds.filter(ad => props.prefer.includes(ad.place));
|
||||
|
||||
if (ads.length === 0) {
|
||||
ads = instance.ads.filter(ad => ad.place === 'square');
|
||||
ads = allAds.filter(ad => ad.place === 'square');
|
||||
}
|
||||
|
||||
const high = ads.filter(ad => ad.priority === 'high');
|
||||
const middle = ads.filter(ad => ad.priority === 'middle');
|
||||
const low = ads.filter(ad => ad.priority === 'low');
|
||||
const lowPriorityAds = ads.filter(ad => ad.ratio === 0);
|
||||
ads = ads.filter(ad => ad.ratio !== 0);
|
||||
|
||||
if (high.length > 0) {
|
||||
ad = high[Math.floor(Math.random() * high.length)];
|
||||
} else if (middle.length > 0) {
|
||||
ad = middle[Math.floor(Math.random() * middle.length)];
|
||||
} else if (low.length > 0) {
|
||||
ad = low[Math.floor(Math.random() * low.length)];
|
||||
if (ads.length === 0) {
|
||||
if (lowPriorityAds.length !== 0) {
|
||||
return lowPriorityAds[Math.floor(Math.random() * lowPriorityAds.length)];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const totalFactor = ads.reduce((a, b) => a + b.ratio, 0);
|
||||
const r = Math.random() * totalFactor;
|
||||
|
||||
let stackedFactor = 0;
|
||||
for (const ad of ads) {
|
||||
if (r >= stackedFactor && r <= stackedFactor + ad.ratio) {
|
||||
return ad;
|
||||
} else {
|
||||
stackedFactor += ad.ratio;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const chosen = ref(choseAd());
|
||||
|
||||
const reduceFrequency = () => {
|
||||
if (chosen.value == null) return;
|
||||
if (defaultStore.state.mutedAds.includes(chosen.value.id)) return;
|
||||
defaultStore.push('mutedAds', chosen.value.id);
|
||||
os.success();
|
||||
chosen.value = choseAd();
|
||||
showMenu.value = false;
|
||||
};
|
||||
|
||||
return {
|
||||
ad,
|
||||
ad: chosen,
|
||||
showMenu,
|
||||
toggleMenu,
|
||||
host,
|
||||
reduceFrequency,
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -157,6 +190,10 @@ export default defineComponent({
|
||||
margin: 0 auto;
|
||||
max-width: 400px;
|
||||
border: solid 1px var(--divider);
|
||||
|
||||
> .button {
|
||||
margin: 8px auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -9,7 +9,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="gqnyydlz" :style="{ background: color }" v-else>
|
||||
<i class="fas fa-eye-slash" @click="hide = true"></i>
|
||||
<a
|
||||
:href="image.url"
|
||||
:title="image.name"
|
||||
@@ -18,6 +17,7 @@
|
||||
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.name" :title="image.name" :cover="false"/>
|
||||
<div class="gif" v-if="image.type === 'image/gif'">GIF</div>
|
||||
</a>
|
||||
<i class="fas fa-eye-slash" @click="hide = true"></i>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<transition :name="$store.state.animation ? popup ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? popup ? 500 : 300 : 0" appear @after-leave="onClosed" @enter="$emit('opening')" @after-enter="childRendered">
|
||||
<div v-show="manualShowing != null ? manualShowing : showing" class="mk-modal" v-hotkey.global="keymap" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
|
||||
<div v-show="manualShowing != null ? manualShowing : showing" class="qzhlnise" :class="{ front }" v-hotkey.global="keymap" :style="{ pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
|
||||
<div class="bg _modalBg" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
|
||||
<div class="content" :class="{ popup, fixed, top: position === 'top' }" @click.self="onBgClick" ref="content">
|
||||
<slot></slot>
|
||||
@@ -41,6 +41,11 @@ export default defineComponent({
|
||||
},
|
||||
position: {
|
||||
required: false
|
||||
},
|
||||
front: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
emits: ['opening', 'click', 'esc', 'close', 'closed'],
|
||||
@@ -224,7 +229,7 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
.mk-modal {
|
||||
.qzhlnise {
|
||||
> .bg {
|
||||
z-index: 10000;
|
||||
}
|
||||
@@ -269,5 +274,19 @@ export default defineComponent({
|
||||
position: fixed;
|
||||
}
|
||||
}
|
||||
|
||||
&.front {
|
||||
> .bg {
|
||||
z-index: 20000;
|
||||
}
|
||||
|
||||
> .content:not(.popup) {
|
||||
z-index: 20000;
|
||||
}
|
||||
|
||||
> .content.popup {
|
||||
z-index: 20000;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -3,10 +3,17 @@ import { api } from './os';
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
type Instance = {
|
||||
export type Instance = {
|
||||
emojis: {
|
||||
category: string;
|
||||
}[];
|
||||
ads: {
|
||||
id: string;
|
||||
ratio: number;
|
||||
place: string;
|
||||
url: string;
|
||||
imageUrl: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
const data = localStorage.getItem('instance');
|
||||
|
@@ -15,12 +15,17 @@
|
||||
<MkRadio v-model="ad.place" value="horizontal">horizontal</MkRadio>
|
||||
<MkRadio v-model="ad.place" value="horizontal-big">horizontal-big</MkRadio>
|
||||
</div>
|
||||
<!--
|
||||
<div style="margin: 32px 0;">
|
||||
{{ $ts.priority }}
|
||||
<MkRadio v-model="ad.priority" value="high">{{ $ts.high }}</MkRadio>
|
||||
<MkRadio v-model="ad.priority" value="middle">{{ $ts.middle }}</MkRadio>
|
||||
<MkRadio v-model="ad.priority" value="low">{{ $ts.low }}</MkRadio>
|
||||
</div>
|
||||
-->
|
||||
<MkInput v-model:value="ad.ratio" type="number">
|
||||
<span>{{ $ts.ratio }}</span>
|
||||
</MkInput>
|
||||
<MkInput v-model:value="ad.expiresAt" type="date">
|
||||
<span>{{ $ts.expiration }}</span>
|
||||
</MkInput>
|
||||
@@ -82,6 +87,7 @@ export default defineComponent({
|
||||
memo: '',
|
||||
place: 'square',
|
||||
priority: 'middle',
|
||||
ratio: 1,
|
||||
url: '',
|
||||
imageUrl: null,
|
||||
expiresAt: null,
|
||||
|
@@ -43,6 +43,7 @@
|
||||
<FormGroup>
|
||||
<template #label>{{ $ts.info }}</template>
|
||||
<FormLink :active="page === 'database'" replace to="/instance/database"><template #icon><i class="fas fa-database"></i></template>{{ $ts.database }}</FormLink>
|
||||
<FormLink :active="page === 'logs'" replace to="/instance/logs"><template #icon><i class="fas fa-stream"></i></template>{{ $ts.logs }}</FormLink>
|
||||
</FormGroup>
|
||||
</FormBase>
|
||||
</div>
|
||||
@@ -105,6 +106,7 @@ export default defineComponent({
|
||||
case 'announcements': return defineAsyncComponent(() => import('./announcements.vue'));
|
||||
case 'ads': return defineAsyncComponent(() => import('./ads.vue'));
|
||||
case 'database': return defineAsyncComponent(() => import('./database.vue'));
|
||||
case 'logs': return defineAsyncComponent(() => import('./logs.vue'));
|
||||
case 'abuses': return defineAsyncComponent(() => import('./abuses.vue'));
|
||||
case 'settings': return defineAsyncComponent(() => import('./settings.vue'));
|
||||
case 'files-settings': return defineAsyncComponent(() => import('./files-settings.vue'));
|
||||
|
@@ -123,7 +123,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { defineComponent, markRaw } from 'vue';
|
||||
import Chart from 'chart.js';
|
||||
import XModalWindow from '@client/components/ui/modal-window.vue';
|
||||
import MkUsersDialog from '@client/components/users-dialog.vue';
|
||||
@@ -280,7 +280,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
this.chartInstance = new Chart(this.canvas, {
|
||||
this.chartInstance = markRaw(new Chart(this.canvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: new Array(chartLimit).fill(0).map((_, i) => this.getDate(i).toLocaleString()).slice().reverse(),
|
||||
@@ -331,7 +331,7 @@ export default defineComponent({
|
||||
mode: 'index',
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
},
|
||||
|
||||
getDate(ago: number) {
|
||||
|
@@ -5,13 +5,13 @@
|
||||
<span>{{ $ts.domain }}</span>
|
||||
</MkInput>
|
||||
<MkSelect v-model:value="logLevel">
|
||||
<template #label>{{ $ts.level }}</template>
|
||||
<option value="all">{{ $ts.levels.all }}</option>
|
||||
<option value="info">{{ $ts.levels.info }}</option>
|
||||
<option value="success">{{ $ts.levels.success }}</option>
|
||||
<option value="warning">{{ $ts.levels.warning }}</option>
|
||||
<option value="error">{{ $ts.levels.error }}</option>
|
||||
<option value="debug">{{ $ts.levels.debug }}</option>
|
||||
<template #label>Level</template>
|
||||
<option value="all">All</option>
|
||||
<option value="info">Info</option>
|
||||
<option value="success">Success</option>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="error">Error</option>
|
||||
<option value="debug">Debug</option>
|
||||
</MkSelect>
|
||||
</div>
|
||||
|
||||
@@ -45,6 +45,8 @@ export default defineComponent({
|
||||
MkTextarea,
|
||||
},
|
||||
|
||||
emits: ['info'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
[symbols.PAGE_INFO]: {
|
||||
@@ -72,6 +74,10 @@ export default defineComponent({
|
||||
this.fetchLogs();
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.$emit('info', this[symbols.PAGE_INFO]);
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchLogs() {
|
||||
os.api('admin/logs', {
|
||||
|
@@ -27,7 +27,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { defineComponent, markRaw } from 'vue';
|
||||
import Chart from 'chart.js';
|
||||
import number from '../../filters/number';
|
||||
|
||||
@@ -69,7 +69,7 @@ export default defineComponent({
|
||||
|
||||
Chart.defaults.global.defaultFontColor = getComputedStyle(document.documentElement).getPropertyValue('--fg');
|
||||
|
||||
this.chart = new Chart(this.$refs.chart, {
|
||||
this.chart = markRaw(new Chart(this.$refs.chart, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
@@ -152,7 +152,7 @@ export default defineComponent({
|
||||
mode: 'index',
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
||||
this.connection.on('stats', this.onStats);
|
||||
this.connection.on('statsLog', this.onStatsLog);
|
||||
|
@@ -55,6 +55,10 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||
where: 'account',
|
||||
default: []
|
||||
},
|
||||
mutedAds: {
|
||||
where: 'account',
|
||||
default: [] as string[]
|
||||
},
|
||||
|
||||
menu: {
|
||||
where: 'deviceAccount',
|
||||
|
@@ -64,11 +64,8 @@ export default defineComponent({
|
||||
<style lang="scss" scoped>
|
||||
.wtdtxvec {
|
||||
--margin: 8px;
|
||||
--panelShadow: none;
|
||||
|
||||
padding: 0 var(--margin);
|
||||
|
||||
::v-deep(._panel) {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -114,6 +114,7 @@ export default defineComponent({
|
||||
overflow: hidden;
|
||||
font-size: 0.9em;
|
||||
color: var(--fg);
|
||||
padding-right: 8px;
|
||||
|
||||
> .a {
|
||||
display: block;
|
||||
@@ -129,6 +130,9 @@ export default defineComponent({
|
||||
font-size: 75%;
|
||||
opacity: 0.7;
|
||||
line-height: $bodyInfoHieght;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# AiScript
|
||||
|
||||
## 関数
|
||||
デフォルトで値渡しです。
|
||||
## Fungsi
|
||||
Secara bawaan, berjalan sebagai pass dari value.
|
||||
|
@@ -1,19 +1,19 @@
|
||||
# Misskey API
|
||||
|
||||
MisskeyAPIを使ってMisskeyクライアント、Misskey連携Webサービス、Bot等(以下「アプリケーション」と呼びます)を開発できます。 ストリーミングAPIもあるので、リアルタイム性のあるアプリケーションを作ることも可能です。
|
||||
Dengan menggunakan Misskey API kamu dapat mengembangkan klien Misskey, Webservice yang terintegrasi dengan Misskey, Bots (nantinya akan disebut "Aplikasi" disini), dll. Terdapat juga streaming API, yang memungkinan untuk membuat aplikasi real-time.
|
||||
|
||||
APIを使い始めるには、まずアクセストークンを取得する必要があります。 このドキュメントでは、アクセストークンを取得する手順を説明した後、基本的なAPIの使い方を説明します。
|
||||
Untuk memulai menggunakin API, kamu harus memiliki access token terlebih dahulu. Halaman ini akan menjelaskan bagaimana untuk mendapatkan access token dan menjelaskan instruksi penggunaan dasar dari Misskey API.
|
||||
|
||||
## アクセストークンの取得
|
||||
基本的に、APIはリクエストにはアクセストークンが必要となります。 APIにリクエストするのが自分自身なのか、不特定の利用者に使ってもらうアプリケーションなのかによって取得手順は異なります。
|
||||
## Mendapatkan access token
|
||||
Pada dasarnya, semua request API membutuhkan access token. Metode untuk mendapatkan sebuah access token bermacam-macam bergantung pada kamu sendiri yang mengirim request API atau request tersebut dikirim melalui aplikasi yang dipakai oleh pengguna akhir.
|
||||
|
||||
* 前者の場合: [「自分自身のアクセストークンを手動発行する」](#自分自身のアクセストークンを手動発行する)に進む
|
||||
* 後者の場合: [「アプリケーション利用者にアクセストークンの発行をリクエストする」](#アプリケーション利用者にアクセストークンの発行をリクエストする)に進む
|
||||
* Apabila kamu pengguna lama: Langsung saja menuju [ "Menerbitkan access token untuk akun kamu sendiri secara manual" ](#自分自身のアクセストークンを手動発行する)
|
||||
* Apabila kamu pengguna baru: Langsung saja menuju [ "Meminta aplikasi pengguna untuk menghasilkan access token" ](#アプリケーション利用者にアクセストークンの発行をリクエストする)
|
||||
|
||||
### 自分自身のアクセストークンを手動発行する
|
||||
「設定 > API」で、自分のアクセストークンを発行できます。
|
||||
### Menerbitkan access token untuk akun kamu sendiri secara manual
|
||||
Kamu dapat membuat access token untuk akun milikmu di Pengaturan > API.
|
||||
|
||||
[「APIの使い方」へ進む](#APIの使い方)
|
||||
[Lanjutkan untuk menggunakan API.](#APIの使い方)
|
||||
|
||||
### アプリケーション利用者にアクセストークンの発行をリクエストする
|
||||
アプリケーション利用者のアクセストークンを取得するには、以下の手順で発行をリクエストします。
|
||||
@@ -49,7 +49,7 @@ UUIDを生成する。以後これをセッションIDと呼びます。
|
||||
* `token` ... ユーザーのアクセストークン
|
||||
* `user` ... ユーザーの情報
|
||||
|
||||
[「APIの使い方」へ進む](#APIの使い方)
|
||||
[Lanjutkan untuk menggunakan API.](#APIの使い方)
|
||||
|
||||
## APIの使い方
|
||||
**APIはすべてPOSTで、リクエスト/レスポンスともにJSON形式です。RESTではありません。** アクセストークンは、`i`というパラメータ名でリクエストに含めます。
|
||||
|
@@ -1,7 +1,7 @@
|
||||
# プラグインの作成
|
||||
Misskey Webクライアントのプラグイン機能を使うと、クライアントを拡張し、様々な機能を追加できます。 ここではプラグインの作成にあたってのメタデータ定義や、AiScript APIリファレンスを掲載します。
|
||||
|
||||
## メタデータ
|
||||
## Metadata
|
||||
プラグインは、AiScriptのメタデータ埋め込み機能を使って、デフォルトとしてプラグインのメタデータを定義する必要があります。 メタデータは次のプロパティを含むオブジェクトです。
|
||||
|
||||
### name
|
||||
|
@@ -1,2 +1,2 @@
|
||||
# Emoji kustom
|
||||
カスタム絵文字は、インスタンスで用意された画像を絵文字のように使える機能です。 ノート、リアクション、チャット、自己紹介、名前などの場所で使うことができます。 カスタム絵文字をそれらの場所で使うには、絵文字ピッカーボタン(ある場合)を押すか、`:`を入力して絵文字サジェストを表示します。 テキスト内に`:foo:`のような形式の文字列が見つかると、`foo`の部分がカスタム絵文字名と解釈され、表示時には対応したカスタム絵文字に置き換わります。
|
||||
Emoji kustom merupakan fungsi yang menyediakan gambar terunggah ke server untuk digunakan seperti emoji. Emoji kustom ini dapt digunakan pada note, reaksi, obrolan, profil, dan bahkan username serta tempat-tempat lainnya. Untuk menggunakan emoji kustom pada tempat yang disebutkan di atas, tekan tombol pemilih Emoji (bila ada) atau ketik sebuah `:` dan jendela saran emoji akan muncul. Jika sebuah string yang terlihat seperti `:foo:` ditemukan dalam text apapun, maka sebagian dari `foo` diinterpretaskan sebagai nama emoji kustom dan akan digantikan dengan gambar emoji kustom ketika ditampilkan.
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# デッキ
|
||||
# Dek
|
||||
|
||||
デッキは利用可能なUIのひとつです。「カラム」と呼ばれるビューを複数並べて表示させることで、カスタマイズ性が高く、情報量の多いUIが構築できることが特徴です。
|
||||
|
||||
|
@@ -1,2 +1,2 @@
|
||||
# Ikuti
|
||||
ユーザーをフォローすると、タイムラインにそのユーザーの投稿が表示されるようになります。ただし、他のユーザーに対する返信は含まれません。 ユーザーをフォローするには、ユーザーページの「フォロー」ボタンをクリックします。フォローを解除するには、もう一度クリックします。
|
||||
Jika kamu mengikuti seorang pengguna, postingan dari pengguna tersebut akan muncul pada linimasa kamu.Akan tetapi, balasan dari mereka kepada pengguna lain tidak akan ditampilkan. Untuk mengikuti seorang pengguna, klik tombol "Ikuti" pada halaman pengguna mereka.Untuk berhenti mengikuti seorang pengguna, klik tombol tersebut sekali lagi.
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# キーボードショートカット
|
||||
|
||||
## グローバル
|
||||
## Global
|
||||
これらのショートカットは基本的にどこでも使えます。
|
||||
<table>
|
||||
<thead>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Pages
|
||||
|
||||
## 変数
|
||||
## Variabel
|
||||
変数を使うことで動的なページを作成できます。テキスト内で <b>{ 変数名 }</b> と書くとそこに変数の値を埋め込めます。例えば <b>Hello { thing } world!</b> というテキストで、変数(thing)の値が <b>ai</b> だった場合、テキストは <b>Hello ai world!</b> になります。
|
||||
|
||||
変数の評価(値を算出すること)は上から下に行われるので、ある変数の中で自分より下の変数を参照することはできません。例えば上から <b>A、B、C</b> と3つの変数を定義したとき、<b>C</b>の中で<b>A</b>や<b>B</b>を参照することはできますが、<b>A</b>の中で<b>B</b>や<b>C</b>を参照することはできません。
|
||||
|
@@ -110,7 +110,7 @@ y = Math.floor(pos / mapWidth)
|
||||
```
|
||||
|
||||
### フォームコントロールの種類
|
||||
#### スイッチ
|
||||
#### Beralih
|
||||
type: `switch` スイッチを表示します。何かの機能をオン/オフさせたい場合に有用です。
|
||||
|
||||
##### プロパティ
|
||||
|
@@ -31,7 +31,7 @@
|
||||
|
||||
**ストリームでのやり取りはすべてJSONです。**
|
||||
|
||||
## チャンネル
|
||||
## Kanal
|
||||
MisskeyのストリーミングAPIにはチャンネルという概念があります。これは、送受信する情報を分離するための仕組みです。 Misskeyのストリームに接続しただけでは、まだリアルタイムでタイムラインの投稿を受信したりはできません。 ストリーム上でチャンネルに接続することで、様々な情報を受け取ったり情報を送信したりすることができるようになります。
|
||||
|
||||
### チャンネルに接続する
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# テーマ
|
||||
# Tema
|
||||
|
||||
テーマを設定して、Misskeyクライアントの見た目を変更できます。
|
||||
|
||||
@@ -61,8 +61,8 @@
|
||||
* 関数(後述)
|
||||
* `:{関数名}<{引数}<{色}`
|
||||
|
||||
#### 定数
|
||||
#### Konstanta
|
||||
「CSS変数として出力はしたくないが、他のCSS変数の値として使いまわしたい」値があるときは、定数を使うと便利です。 キー名を`$`で始めると、そのキーはCSS変数として出力されません。
|
||||
|
||||
#### 関数
|
||||
#### Fungsi
|
||||
wip
|
||||
|
@@ -1,15 +1,15 @@
|
||||
# タイムラインの比較
|
||||
# Perbandingan Linimasa
|
||||
|
||||
https://docs.google.com/spreadsheets/d/1lxQ2ugKrhz58Bg96HTDK_2F98BUritkMyIiBkOByjHA/edit?usp=sharing
|
||||
|
||||
## ホーム
|
||||
自分のフォローしているユーザーの投稿
|
||||
## Beranda
|
||||
Postingan dari pengguna yang kamu ikuti
|
||||
|
||||
## ローカル
|
||||
全てのローカルユーザーの「ホーム」指定されていない投稿
|
||||
## Lokal
|
||||
Seluruh postingan dari pengguna lokal yang tidak ditandai sebagai "Hanya Beranda"
|
||||
|
||||
## ソーシャル
|
||||
自分のフォローしているユーザーの投稿と、全てのローカルユーザーの「ホーム」指定されていない投稿
|
||||
## Sosial
|
||||
Postingan dari pengguna yang kamu ikuti beserta dengan semua postingan dari pengguna lokal yang tidak ditandai sebagai "Hanya Beranda"
|
||||
|
||||
## グローバル
|
||||
全てのローカルユーザーの「ホーム」指定されていない投稿と、サーバーに届いた全てのリモートユーザーの「ホーム」指定されていない投稿
|
||||
## Global
|
||||
Seluruh postingan dari pengguna lokal yang tidak ditandai sebagai "Hanya Beranda" dan juga pesan yang diterima oleh server yang tidak ditandai sebagai "Hanya Beranda"
|
||||
|
@@ -8,7 +8,7 @@ Wpisy użytkowników, których obserwujesz
|
||||
## Lokalne
|
||||
全てのローカルユーザーの「ホーム」指定されていない投稿
|
||||
|
||||
## ソーシャル
|
||||
## Społeczność
|
||||
自分のフォローしているユーザーの投稿と、全てのローカルユーザーの「ホーム」指定されていない投稿
|
||||
|
||||
## Globalne
|
||||
|
@@ -55,7 +55,8 @@ export async function getHtml(url: string, accept = 'text/html, */*', timeout =
|
||||
const _http = new http.Agent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30 * 1000,
|
||||
});
|
||||
lookup: cache.lookup,
|
||||
} as http.AgentOptions);
|
||||
|
||||
/**
|
||||
* Get https non-proxy agent
|
||||
@@ -70,14 +71,14 @@ const _https = new https.Agent({
|
||||
* Get http proxy or non-proxy agent
|
||||
*/
|
||||
export const httpAgent = config.proxy
|
||||
? new HttpProxyAgent(config.proxy) as unknown as http.Agent
|
||||
? new HttpProxyAgent(config.proxy)
|
||||
: _http;
|
||||
|
||||
/**
|
||||
* Get https proxy or non-proxy agent
|
||||
*/
|
||||
export const httpsAgent = config.proxy
|
||||
? new HttpsProxyAgent(config.proxy) as unknown as https.Agent
|
||||
? new HttpsProxyAgent(config.proxy)
|
||||
: _https;
|
||||
|
||||
/**
|
||||
|
@@ -23,11 +23,17 @@ export class Ad {
|
||||
})
|
||||
public place: string;
|
||||
|
||||
// 今は使われていないが将来的に活用される可能性はある
|
||||
@Column('varchar', {
|
||||
length: 32, nullable: false
|
||||
})
|
||||
public priority: string;
|
||||
|
||||
@Column('integer', {
|
||||
default: 1, nullable: false
|
||||
})
|
||||
public ratio: number;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024, nullable: false
|
||||
})
|
||||
|
@@ -7,6 +7,8 @@ import { Channel } from './channel';
|
||||
|
||||
@Entity()
|
||||
@Index('IDX_NOTE_TAGS', { synchronize: false })
|
||||
@Index('IDX_NOTE_MENTIONS', { synchronize: false })
|
||||
@Index('IDX_NOTE_VISIBLE_USER_IDS', { synchronize: false })
|
||||
export class Note {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import * as httpSignature from 'http-signature';
|
||||
|
||||
import config from '@/config';
|
||||
import { User } from '../models/entities/user';
|
||||
import { program } from '../argv';
|
||||
|
||||
import processDeliver from './processors/deliver';
|
||||
@@ -11,14 +10,9 @@ import procesObjectStorage from './processors/object-storage';
|
||||
import { queueLogger } from './logger';
|
||||
import { DriveFile } from '../models/entities/drive-file';
|
||||
import { getJobInfo } from './get-job-info';
|
||||
import { IActivity } from '../remote/activitypub/type';
|
||||
import { dbQueue, deliverQueue, inboxQueue, objectStorageQueue } from './queues';
|
||||
|
||||
export type InboxJobData = {
|
||||
activity: IActivity,
|
||||
/** HTTP-Signature */
|
||||
signature: httpSignature.IParsedSignature
|
||||
};
|
||||
import { ThinUser } from './types';
|
||||
import { IActivity } from '@/remote/activitypub/type';
|
||||
|
||||
function renderError(e: Error): any {
|
||||
return {
|
||||
@@ -65,8 +59,9 @@ objectStorageQueue
|
||||
.on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`));
|
||||
|
||||
export function deliver(user: { id: User['id']; host: null; }, content: any, to: any) {
|
||||
export function deliver(user: ThinUser, content: unknown, to: string | null) {
|
||||
if (content == null) return null;
|
||||
if (to == null) return null;
|
||||
|
||||
const data = {
|
||||
user,
|
||||
@@ -76,6 +71,7 @@ export function deliver(user: { id: User['id']; host: null; }, content: any, to:
|
||||
|
||||
return deliverQueue.add(data, {
|
||||
attempts: config.deliverJobMaxAttempts || 12,
|
||||
timeout: 1 * 60 * 1000, // 1min
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 60 * 1000
|
||||
@@ -85,7 +81,7 @@ export function deliver(user: { id: User['id']; host: null; }, content: any, to:
|
||||
});
|
||||
}
|
||||
|
||||
export function inbox(activity: any, signature: httpSignature.IParsedSignature) {
|
||||
export function inbox(activity: IActivity, signature: httpSignature.IParsedSignature) {
|
||||
const data = {
|
||||
activity: activity,
|
||||
signature
|
||||
@@ -93,6 +89,7 @@ export function inbox(activity: any, signature: httpSignature.IParsedSignature)
|
||||
|
||||
return inboxQueue.add(data, {
|
||||
attempts: config.inboxJobMaxAttempts || 8,
|
||||
timeout: 5 * 60 * 1000, // 5min
|
||||
backoff: {
|
||||
type: 'exponential',
|
||||
delay: 60 * 1000
|
||||
@@ -102,7 +99,7 @@ export function inbox(activity: any, signature: httpSignature.IParsedSignature)
|
||||
});
|
||||
}
|
||||
|
||||
export function createDeleteDriveFilesJob(user: { id: User['id'] }) {
|
||||
export function createDeleteDriveFilesJob(user: ThinUser) {
|
||||
return dbQueue.add('deleteDriveFiles', {
|
||||
user: user
|
||||
}, {
|
||||
@@ -111,7 +108,7 @@ export function createDeleteDriveFilesJob(user: { id: User['id'] }) {
|
||||
});
|
||||
}
|
||||
|
||||
export function createExportNotesJob(user: { id: User['id'] }) {
|
||||
export function createExportNotesJob(user: ThinUser) {
|
||||
return dbQueue.add('exportNotes', {
|
||||
user: user
|
||||
}, {
|
||||
@@ -120,7 +117,7 @@ export function createExportNotesJob(user: { id: User['id'] }) {
|
||||
});
|
||||
}
|
||||
|
||||
export function createExportFollowingJob(user: { id: User['id'] }) {
|
||||
export function createExportFollowingJob(user: ThinUser) {
|
||||
return dbQueue.add('exportFollowing', {
|
||||
user: user
|
||||
}, {
|
||||
@@ -129,7 +126,7 @@ export function createExportFollowingJob(user: { id: User['id'] }) {
|
||||
});
|
||||
}
|
||||
|
||||
export function createExportMuteJob(user: { id: User['id'] }) {
|
||||
export function createExportMuteJob(user: ThinUser) {
|
||||
return dbQueue.add('exportMute', {
|
||||
user: user
|
||||
}, {
|
||||
@@ -138,7 +135,7 @@ export function createExportMuteJob(user: { id: User['id'] }) {
|
||||
});
|
||||
}
|
||||
|
||||
export function createExportBlockingJob(user: { id: User['id'] }) {
|
||||
export function createExportBlockingJob(user: ThinUser) {
|
||||
return dbQueue.add('exportBlocking', {
|
||||
user: user
|
||||
}, {
|
||||
@@ -147,7 +144,7 @@ export function createExportBlockingJob(user: { id: User['id'] }) {
|
||||
});
|
||||
}
|
||||
|
||||
export function createExportUserListsJob(user: { id: User['id'] }) {
|
||||
export function createExportUserListsJob(user: ThinUser) {
|
||||
return dbQueue.add('exportUserLists', {
|
||||
user: user
|
||||
}, {
|
||||
@@ -156,7 +153,7 @@ export function createExportUserListsJob(user: { id: User['id'] }) {
|
||||
});
|
||||
}
|
||||
|
||||
export function createImportFollowingJob(user: { id: User['id'] }, fileId: DriveFile['id']) {
|
||||
export function createImportFollowingJob(user: ThinUser, fileId: DriveFile['id']) {
|
||||
return dbQueue.add('importFollowing', {
|
||||
user: user,
|
||||
fileId: fileId
|
||||
@@ -166,7 +163,7 @@ export function createImportFollowingJob(user: { id: User['id'] }, fileId: Drive
|
||||
});
|
||||
}
|
||||
|
||||
export function createImportUserListsJob(user: { id: User['id'] }, fileId: DriveFile['id']) {
|
||||
export function createImportUserListsJob(user: ThinUser, fileId: DriveFile['id']) {
|
||||
return dbQueue.add('importUserLists', {
|
||||
user: user,
|
||||
fileId: fileId
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import * as Queue from 'bull';
|
||||
import * as Bull from 'bull';
|
||||
import config from '@/config';
|
||||
|
||||
export function initialize(name: string, limitPerSec = -1) {
|
||||
return new Queue(name, {
|
||||
export function initialize<T>(name: string, limitPerSec = -1) {
|
||||
return new Bull<T>(name, {
|
||||
redis: {
|
||||
port: config.redis.port,
|
||||
host: config.redis.host,
|
||||
|
@@ -4,10 +4,11 @@ import { queueLogger } from '../../logger';
|
||||
import { deleteFileSync } from '../../../services/drive/delete-file';
|
||||
import { Users, DriveFiles } from '../../../models';
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { DbUserJobData } from '@/queue/types';
|
||||
|
||||
const logger = queueLogger.createSubLogger('delete-drive-files');
|
||||
|
||||
export async function deleteDriveFiles(job: Bull.Job, done: any): Promise<void> {
|
||||
export async function deleteDriveFiles(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
|
||||
logger.info(`Deleting drive files of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await Users.findOne(job.data.user.id);
|
||||
|
@@ -8,10 +8,11 @@ import dateFormat = require('dateformat');
|
||||
import { getFullApAccount } from '@/misc/convert-host';
|
||||
import { Users, Blockings } from '../../../models';
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { DbUserJobData } from '@/queue/types';
|
||||
|
||||
const logger = queueLogger.createSubLogger('export-blocking');
|
||||
|
||||
export async function exportBlocking(job: Bull.Job, done: any): Promise<void> {
|
||||
export async function exportBlocking(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
|
||||
logger.info(`Exporting blocking of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await Users.findOne(job.data.user.id);
|
||||
@@ -61,7 +62,7 @@ export async function exportBlocking(job: Bull.Job, done: any): Promise<void> {
|
||||
}
|
||||
|
||||
const content = getFullApAccount(u.username, u.host);
|
||||
await new Promise((res, rej) => {
|
||||
await new Promise<void>((res, rej) => {
|
||||
stream.write(content + '\n', err => {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
|
@@ -8,10 +8,11 @@ import dateFormat = require('dateformat');
|
||||
import { getFullApAccount } from '@/misc/convert-host';
|
||||
import { Users, Followings } from '../../../models';
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { DbUserJobData } from '@/queue/types';
|
||||
|
||||
const logger = queueLogger.createSubLogger('export-following');
|
||||
|
||||
export async function exportFollowing(job: Bull.Job, done: any): Promise<void> {
|
||||
export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
|
||||
logger.info(`Exporting following of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await Users.findOne(job.data.user.id);
|
||||
@@ -61,7 +62,7 @@ export async function exportFollowing(job: Bull.Job, done: any): Promise<void> {
|
||||
}
|
||||
|
||||
const content = getFullApAccount(u.username, u.host);
|
||||
await new Promise((res, rej) => {
|
||||
await new Promise<void>((res, rej) => {
|
||||
stream.write(content + '\n', err => {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
|
@@ -8,10 +8,11 @@ import dateFormat = require('dateformat');
|
||||
import { getFullApAccount } from '@/misc/convert-host';
|
||||
import { Users, Mutings } from '../../../models';
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { DbUserJobData } from '@/queue/types';
|
||||
|
||||
const logger = queueLogger.createSubLogger('export-mute');
|
||||
|
||||
export async function exportMute(job: Bull.Job, done: any): Promise<void> {
|
||||
export async function exportMute(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
|
||||
logger.info(`Exporting mute of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await Users.findOne(job.data.user.id);
|
||||
@@ -61,7 +62,7 @@ export async function exportMute(job: Bull.Job, done: any): Promise<void> {
|
||||
}
|
||||
|
||||
const content = getFullApAccount(u.username, u.host);
|
||||
await new Promise((res, rej) => {
|
||||
await new Promise<void>((res, rej) => {
|
||||
stream.write(content + '\n', err => {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
|
@@ -9,10 +9,11 @@ import { Users, Notes, Polls } from '../../../models';
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { Note } from '../../../models/entities/note';
|
||||
import { Poll } from '../../../models/entities/poll';
|
||||
import { DbUserJobData } from '@/queue/types';
|
||||
|
||||
const logger = queueLogger.createSubLogger('export-notes');
|
||||
|
||||
export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
|
||||
export async function exportNotes(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
|
||||
logger.info(`Exporting notes of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await Users.findOne(job.data.user.id);
|
||||
@@ -33,7 +34,7 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
|
||||
|
||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
await new Promise<void>((res, rej) => {
|
||||
stream.write('[', err => {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
@@ -72,7 +73,7 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
|
||||
poll = await Polls.findOneOrFail({ noteId: note.id });
|
||||
}
|
||||
const content = JSON.stringify(serialize(note, poll));
|
||||
await new Promise((res, rej) => {
|
||||
await new Promise<void>((res, rej) => {
|
||||
stream.write(exportedNotesCount === 0 ? content : ',\n' + content, err => {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
@@ -92,7 +93,7 @@ export async function exportNotes(job: Bull.Job, done: any): Promise<void> {
|
||||
job.progress(exportedNotesCount / total);
|
||||
}
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
await new Promise<void>((res, rej) => {
|
||||
stream.write(']', err => {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
|
@@ -8,10 +8,11 @@ import dateFormat = require('dateformat');
|
||||
import { getFullApAccount } from '@/misc/convert-host';
|
||||
import { Users, UserLists, UserListJoinings } from '../../../models';
|
||||
import { In } from 'typeorm';
|
||||
import { DbUserJobData } from '@/queue/types';
|
||||
|
||||
const logger = queueLogger.createSubLogger('export-user-lists');
|
||||
|
||||
export async function exportUserLists(job: Bull.Job, done: any): Promise<void> {
|
||||
export async function exportUserLists(job: Bull.Job<DbUserJobData>, done: any): Promise<void> {
|
||||
logger.info(`Exporting user lists of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await Users.findOne(job.data.user.id);
|
||||
@@ -45,7 +46,7 @@ export async function exportUserLists(job: Bull.Job, done: any): Promise<void> {
|
||||
for (const u of users) {
|
||||
const acct = getFullApAccount(u.username, u.host);
|
||||
const content = `${list.name},${acct}`;
|
||||
await new Promise((res, rej) => {
|
||||
await new Promise<void>((res, rej) => {
|
||||
stream.write(content + '\n', err => {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
|
@@ -7,10 +7,11 @@ import { resolveUser } from '../../../remote/resolve-user';
|
||||
import { downloadTextFile } from '@/misc/download-text-file';
|
||||
import { isSelfHost, toPuny } from '@/misc/convert-host';
|
||||
import { Users, DriveFiles } from '../../../models';
|
||||
import { DbUserImportJobData } from '@/queue/types';
|
||||
|
||||
const logger = queueLogger.createSubLogger('import-following');
|
||||
|
||||
export async function importFollowing(job: Bull.Job, done: any): Promise<void> {
|
||||
export async function importFollowing(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
|
||||
logger.info(`Importing following of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await Users.findOne(job.data.user.id);
|
||||
|
@@ -8,10 +8,11 @@ import { downloadTextFile } from '@/misc/download-text-file';
|
||||
import { isSelfHost, toPuny } from '@/misc/convert-host';
|
||||
import { DriveFiles, Users, UserLists, UserListJoinings } from '../../../models';
|
||||
import { genId } from '@/misc/gen-id';
|
||||
import { DbUserImportJobData } from '@/queue/types';
|
||||
|
||||
const logger = queueLogger.createSubLogger('import-user-lists');
|
||||
|
||||
export async function importUserLists(job: Bull.Job, done: any): Promise<void> {
|
||||
export async function importUserLists(job: Bull.Job<DbUserImportJobData>, done: any): Promise<void> {
|
||||
logger.info(`Importing user lists of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await Users.findOne(job.data.user.id);
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import * as Bull from 'bull';
|
||||
import { DbJobData } from '@/queue/types';
|
||||
import { deleteDriveFiles } from './delete-drive-files';
|
||||
import { exportNotes } from './export-notes';
|
||||
import { exportFollowing } from './export-following';
|
||||
@@ -17,10 +18,10 @@ const jobs = {
|
||||
exportUserLists,
|
||||
importFollowing,
|
||||
importUserLists
|
||||
} as any;
|
||||
} as Record<string, Bull.ProcessCallbackFunction<DbJobData> | Bull.ProcessPromiseFunction<DbJobData>>;
|
||||
|
||||
export default function(dbQueue: Bull.Queue) {
|
||||
export default function(dbQueue: Bull.Queue<DbJobData>) {
|
||||
for (const [k, v] of Object.entries(jobs)) {
|
||||
dbQueue.process(k, v as any);
|
||||
dbQueue.process(k, v);
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ import { fetchMeta } from '@/misc/fetch-meta';
|
||||
import { toPuny } from '@/misc/convert-host';
|
||||
import { Cache } from '@/misc/cache';
|
||||
import { Instance } from '../../models/entities/instance';
|
||||
import { DeliverJobData } from '../types';
|
||||
|
||||
const logger = new Logger('deliver');
|
||||
|
||||
@@ -17,7 +18,7 @@ let latest: string | null = null;
|
||||
|
||||
const suspendedHostsCache = new Cache<Instance[]>(1000 * 60 * 60);
|
||||
|
||||
export default async (job: Bull.Job) => {
|
||||
export default async (job: Bull.Job<DeliverJobData>) => {
|
||||
const { host } = new URL(job.data.to);
|
||||
|
||||
// ブロックしてたら中断
|
||||
|
@@ -10,7 +10,7 @@ import { fetchMeta } from '@/misc/fetch-meta';
|
||||
import { toPuny, extractDbHost } from '@/misc/convert-host';
|
||||
import { getApId } from '../../remote/activitypub/type';
|
||||
import { fetchInstanceMetadata } from '../../services/fetch-instance-metadata';
|
||||
import { InboxJobData } from '..';
|
||||
import { InboxJobData } from '../types';
|
||||
import DbResolver from '../../remote/activitypub/db-resolver';
|
||||
import { resolvePerson } from '../../remote/activitypub/models/person';
|
||||
import { LdSignature } from '../../remote/activitypub/misc/ld-signature';
|
||||
@@ -23,7 +23,7 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
|
||||
const activity = job.data.activity;
|
||||
|
||||
//#region Log
|
||||
const info = Object.assign({}, activity);
|
||||
const info = Object.assign({}, activity) as any;
|
||||
delete info['@context'];
|
||||
logger.debug(JSON.stringify(info, null, 2));
|
||||
//#endregion
|
||||
|
@@ -7,7 +7,7 @@ import { MoreThan, Not, IsNull } from 'typeorm';
|
||||
|
||||
const logger = queueLogger.createSubLogger('clean-remote-files');
|
||||
|
||||
export default async function cleanRemoteFiles(job: Bull.Job, done: any): Promise<void> {
|
||||
export default async function cleanRemoteFiles(job: Bull.Job<{}>, done: any): Promise<void> {
|
||||
logger.info(`Deleting cached remote files...`);
|
||||
|
||||
let deletedCount = 0;
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { ObjectStorageFileJobData } from '@/queue/types';
|
||||
import * as Bull from 'bull';
|
||||
import { deleteObjectStorageFile } from '../../../services/drive/delete-file';
|
||||
|
||||
export default async (job: Bull.Job) => {
|
||||
export default async (job: Bull.Job<ObjectStorageFileJobData>) => {
|
||||
const key: string = job.data.key;
|
||||
|
||||
await deleteObjectStorageFile(key);
|
||||
|
@@ -1,14 +1,15 @@
|
||||
import * as Bull from 'bull';
|
||||
import { ObjectStorageJobData } from '@/queue/types';
|
||||
import deleteFile from './delete-file';
|
||||
import cleanRemoteFiles from './clean-remote-files';
|
||||
|
||||
const jobs = {
|
||||
deleteFile,
|
||||
cleanRemoteFiles,
|
||||
} as any;
|
||||
} as Record<string, Bull.ProcessCallbackFunction<ObjectStorageJobData> | Bull.ProcessPromiseFunction<ObjectStorageJobData>>;
|
||||
|
||||
export default function(q: Bull.Queue) {
|
||||
for (const [k, v] of Object.entries(jobs)) {
|
||||
q.process(k, 16, v as any);
|
||||
q.process(k, 16, v);
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import config from '@/config';
|
||||
import { initialize as initializeQueue } from './initialize';
|
||||
import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData } from './types';
|
||||
|
||||
export const deliverQueue = initializeQueue('deliver', config.deliverJobPerSec || 128);
|
||||
export const inboxQueue = initializeQueue('inbox', config.inboxJobPerSec || 16);
|
||||
export const dbQueue = initializeQueue('db');
|
||||
export const objectStorageQueue = initializeQueue('objectStorage');
|
||||
export const deliverQueue = initializeQueue<DeliverJobData>('deliver', config.deliverJobPerSec || 128);
|
||||
export const inboxQueue = initializeQueue<InboxJobData>('inbox', config.inboxJobPerSec || 16);
|
||||
export const dbQueue = initializeQueue<DbJobData>('db');
|
||||
export const objectStorageQueue = initializeQueue<ObjectStorageJobData>('objectStorage');
|
||||
|
39
src/queue/types.ts
Normal file
39
src/queue/types.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { DriveFile } from '@/models/entities/drive-file';
|
||||
import { User } from '@/models/entities/user';
|
||||
import { IActivity } from '@/remote/activitypub/type';
|
||||
import * as httpSignature from 'http-signature';
|
||||
|
||||
export type DeliverJobData = {
|
||||
/** Actor */
|
||||
user: ThinUser;
|
||||
/** Activity */
|
||||
content: unknown;
|
||||
/** inbox URL to deliver */
|
||||
to: string;
|
||||
};
|
||||
|
||||
export type InboxJobData = {
|
||||
activity: IActivity;
|
||||
signature: httpSignature.IParsedSignature;
|
||||
};
|
||||
|
||||
export type DbJobData = DbUserJobData | DbUserImportJobData;
|
||||
|
||||
export type DbUserJobData = {
|
||||
user: ThinUser;
|
||||
};
|
||||
|
||||
export type DbUserImportJobData = {
|
||||
user: ThinUser;
|
||||
fileId: DriveFile['id'];
|
||||
};
|
||||
|
||||
export type ObjectStorageJobData = ObjectStorageFileJobData | {};
|
||||
|
||||
export type ObjectStorageFileJobData = {
|
||||
key: string;
|
||||
};
|
||||
|
||||
export type ThinUser = {
|
||||
id: User['id'];
|
||||
};
|
@@ -11,6 +11,11 @@ export default async (actor: IRemoteUser, activity: ILike) => {
|
||||
|
||||
await extractEmojis(activity.tag || [], actor.host).catch(() => null);
|
||||
|
||||
await create(actor, note, activity._misskey_reaction || activity.content || activity.name);
|
||||
return `ok`;
|
||||
return await create(actor, note, activity._misskey_reaction || activity.content || activity.name).catch(e => {
|
||||
if (e.id === '51c42bb4-931a-456b-bff7-e5a8a70dd298') {
|
||||
return 'skip: already reacted';
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}).then(() => 'ok');
|
||||
};
|
||||
|
@@ -511,6 +511,10 @@ const activitystreams = {
|
||||
"shares": {
|
||||
"@id": "as:shares",
|
||||
"@type": "@id"
|
||||
},
|
||||
"alsoKnownAs": {
|
||||
"@id": "as:alsoKnownAs",
|
||||
"@type": "@id"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -15,7 +15,33 @@ export const renderActivity = (x: any): IActivity | null => {
|
||||
return Object.assign({
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1'
|
||||
'https://w3id.org/security/v1',
|
||||
{
|
||||
// as non-standards
|
||||
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
|
||||
sensitive: 'as:sensitive',
|
||||
Hashtag: 'as:Hashtag',
|
||||
quoteUrl: 'as:quoteUrl',
|
||||
// Mastodon
|
||||
toot: 'http://joinmastodon.org/ns#',
|
||||
Emoji: 'toot:Emoji',
|
||||
featured: 'toot:featured',
|
||||
discoverable: 'toot:discoverable',
|
||||
// schema
|
||||
schema: 'http://schema.org#',
|
||||
PropertyValue: 'schema:PropertyValue',
|
||||
value: 'schema:value',
|
||||
// Misskey
|
||||
misskey: `${config.url}/ns#`,
|
||||
'_misskey_content': 'misskey:_misskey_content',
|
||||
'_misskey_quote': 'misskey:_misskey_quote',
|
||||
'_misskey_reaction': 'misskey:_misskey_reaction',
|
||||
'_misskey_votes': 'misskey:_misskey_votes',
|
||||
'_misskey_talk': 'misskey:_misskey_talk',
|
||||
'isCat': 'misskey:isCat',
|
||||
// vcard
|
||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||
}
|
||||
]
|
||||
}, x);
|
||||
};
|
||||
@@ -25,35 +51,6 @@ export const attachLdSignature = async (activity: any, user: { id: User['id']; h
|
||||
|
||||
const keypair = await getUserKeypair(user.id);
|
||||
|
||||
const obj = {
|
||||
// as non-standards
|
||||
manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
|
||||
sensitive: 'as:sensitive',
|
||||
Hashtag: 'as:Hashtag',
|
||||
quoteUrl: 'as:quoteUrl',
|
||||
// Mastodon
|
||||
toot: 'http://joinmastodon.org/ns#',
|
||||
Emoji: 'toot:Emoji',
|
||||
featured: 'toot:featured',
|
||||
discoverable: 'toot:discoverable',
|
||||
// schema
|
||||
schema: 'http://schema.org#',
|
||||
PropertyValue: 'schema:PropertyValue',
|
||||
value: 'schema:value',
|
||||
// Misskey
|
||||
misskey: `${config.url}/ns#`,
|
||||
'_misskey_content': 'misskey:_misskey_content',
|
||||
'_misskey_quote': 'misskey:_misskey_quote',
|
||||
'_misskey_reaction': 'misskey:_misskey_reaction',
|
||||
'_misskey_votes': 'misskey:_misskey_votes',
|
||||
'_misskey_talk': 'misskey:_misskey_talk',
|
||||
'isCat': 'misskey:isCat',
|
||||
// vcard
|
||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||
};
|
||||
|
||||
activity['@context'].push(obj);
|
||||
|
||||
const ldSignature = new LdSignature();
|
||||
ldSignature.debug = false;
|
||||
activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${config.url}/users/${user.id}#main-key`);
|
||||
|
@@ -1,8 +1,12 @@
|
||||
import config from '@/config';
|
||||
import { ILocalUser, User } from '../../../models/entities/user';
|
||||
|
||||
export default (object: any, user: { id: User['id'] }) => ({
|
||||
type: 'Undo',
|
||||
actor: `${config.url}/users/${user.id}`,
|
||||
object
|
||||
});
|
||||
export default (object: any, user: { id: User['id'] }) => {
|
||||
if (object == null) return null;
|
||||
|
||||
return {
|
||||
type: 'Undo',
|
||||
actor: `${config.url}/users/${user.id}`,
|
||||
object
|
||||
};
|
||||
};
|
||||
|
@@ -22,7 +22,7 @@ export function generateVisibilityQuery(q: SelectQueryBuilder<any>, me?: { id: U
|
||||
// または 自分自身
|
||||
.orWhere('note.userId = :userId1', { userId1: me.id })
|
||||
// または 自分宛て
|
||||
.orWhere(':userId2 = ANY(note.visibleUserIds)', { userId2: me.id })
|
||||
.orWhere(`'{"${me.id}"}' <@ note.visibleUserIds`)
|
||||
.orWhere(new Brackets(qb => { qb
|
||||
// または フォロワー宛ての投稿であり、
|
||||
.where('note.visibility = \'followers\'')
|
||||
|
@@ -22,6 +22,9 @@ export const meta = {
|
||||
priority: {
|
||||
validator: $.str
|
||||
},
|
||||
ratio: {
|
||||
validator: $.num.int().min(0)
|
||||
},
|
||||
expiresAt: {
|
||||
validator: $.num.int()
|
||||
},
|
||||
@@ -39,6 +42,7 @@ export default define(meta, async (ps) => {
|
||||
url: ps.url,
|
||||
imageUrl: ps.imageUrl,
|
||||
priority: ps.priority,
|
||||
ratio: ps.ratio,
|
||||
place: ps.place,
|
||||
memo: ps.memo,
|
||||
});
|
||||
|
@@ -29,6 +29,9 @@ export const meta = {
|
||||
priority: {
|
||||
validator: $.str
|
||||
},
|
||||
ratio: {
|
||||
validator: $.num.int().min(0)
|
||||
},
|
||||
expiresAt: {
|
||||
validator: $.num.int()
|
||||
},
|
||||
@@ -52,6 +55,7 @@ export default define(meta, async (ps, me) => {
|
||||
url: ps.url,
|
||||
place: ps.place,
|
||||
priority: ps.priority,
|
||||
ratio: ps.ratio,
|
||||
memo: ps.memo,
|
||||
imageUrl: ps.imageUrl,
|
||||
expiresAt: new Date(ps.expiresAt),
|
||||
|
26
src/server/api/endpoints/admin/get-index-stats.ts
Normal file
26
src/server/api/endpoints/admin/get-index-stats.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import define from '../../define';
|
||||
import { getConnection } from 'typeorm';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true as const,
|
||||
requireModerator: true,
|
||||
|
||||
tags: ['admin'],
|
||||
|
||||
params: {
|
||||
},
|
||||
};
|
||||
|
||||
export default define(meta, async () => {
|
||||
const stats = await
|
||||
getConnection().query(`SELECT * FROM pg_indexes;`)
|
||||
.then(recs => {
|
||||
const res = [] as { tablename: string; indexname: string; }[];
|
||||
for (const rec of recs) {
|
||||
res.push(rec);
|
||||
}
|
||||
return res;
|
||||
});
|
||||
|
||||
return stats;
|
||||
});
|
@@ -509,9 +509,10 @@ export default define(meta, async (ps, me) => {
|
||||
maxNoteTextLength: Math.min(instance.maxNoteTextLength, DB_MAX_NOTE_TEXT_LENGTH),
|
||||
emojis: await Emojis.packMany(emojis),
|
||||
ads: ads.map(ad => ({
|
||||
id: ad.id,
|
||||
url: ad.url,
|
||||
place: ad.place,
|
||||
priority: ad.priority,
|
||||
ratio: ad.ratio,
|
||||
imageUrl: ad.imageUrl,
|
||||
})),
|
||||
enableEmail: instance.enableEmail,
|
||||
|
@@ -60,8 +60,8 @@ export default define(meta, async (ps, user) => {
|
||||
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.andWhere(new Brackets(qb => { qb
|
||||
.where(`:meId = ANY(note.mentions)`, { meId: user.id })
|
||||
.orWhere(`:meId = ANY(note.visibleUserIds)`, { meId: user.id });
|
||||
.where(`'{"${user.id}"}' <@ note.mentions`)
|
||||
.orWhere(`'{"${user.id}"}' <@ note.visibleUserIds`);
|
||||
}))
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('note.reply', 'reply')
|
||||
|
@@ -35,7 +35,7 @@ export default async function(user: User, note: Note, quiet = false) {
|
||||
});
|
||||
|
||||
//#region ローカルの投稿なら削除アクティビティを配送
|
||||
if (Users.isLocalUser(user)) {
|
||||
if (Users.isLocalUser(user) && !note.localOnly) {
|
||||
let renote: Note | undefined;
|
||||
|
||||
// if deletd note is renote
|
||||
|
@@ -13,12 +13,13 @@ import { createNotification } from '../../create-notification';
|
||||
import deleteReaction from './delete';
|
||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error';
|
||||
import { NoteReaction } from '../../../models/entities/note-reaction';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error';
|
||||
|
||||
export default async (user: { id: User['id']; host: User['host']; }, note: Note, reaction?: string) => {
|
||||
// TODO: cache
|
||||
reaction = await toDbReaction(reaction, user.host);
|
||||
|
||||
let record: NoteReaction = {
|
||||
const record: NoteReaction = {
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
noteId: note.id,
|
||||
@@ -31,17 +32,18 @@ export default async (user: { id: User['id']; host: User['host']; }, note: Note,
|
||||
await NoteReactions.insert(record);
|
||||
} catch (e) {
|
||||
if (isDuplicateKeyValueError(e)) {
|
||||
record = await NoteReactions.findOneOrFail({
|
||||
const exists = await NoteReactions.findOneOrFail({
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (record.reaction !== reaction) {
|
||||
if (exists.reaction !== reaction) {
|
||||
// 別のリアクションがすでにされていたら置き換える
|
||||
await deleteReaction(user, note);
|
||||
await NoteReactions.insert(record);
|
||||
} else {
|
||||
// 同じリアクションがすでにされていたら何もしない
|
||||
return;
|
||||
// 同じリアクションがすでにされていたらエラー
|
||||
throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298');
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
|
Reference in New Issue
Block a user