summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-03-22 15:27:08 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2021-03-22 15:27:08 +0900
commit52d577c7dd7bf87b3fae34f539bb6e656c7c0ed2 (patch)
tree9805c625a7fba9d8631db8a92772b2772d8632ec /src
parentMerge branch 'develop' (diff)
parent12.75.0 (diff)
downloadmisskey-52d577c7dd7bf87b3fae34f539bb6e656c7c0ed2.tar.gz
misskey-52d577c7dd7bf87b3fae34f539bb6e656c7c0ed2.tar.bz2
misskey-52d577c7dd7bf87b3fae34f539bb6e656c7c0ed2.zip
Merge branch 'develop'
Diffstat (limited to 'src')
-rw-r--r--src/.eslintrc6
-rw-r--r--src/client/.eslintrc20
-rw-r--r--src/client/components/note-detailed.vue3
-rw-r--r--src/client/components/note.vue3
-rw-r--r--src/client/components/ui/modal.vue2
-rw-r--r--src/client/init.ts34
-rw-r--r--src/client/pages/doc.vue2
-rw-r--r--src/client/pages/page-editor/page-editor.vue22
-rw-r--r--src/client/store.ts1
-rw-r--r--src/client/theme-store.ts27
-rw-r--r--src/client/ui/chat/note.vue3
-rw-r--r--src/daemons/janitor.ts2
-rw-r--r--src/daemons/queue-stats.ts2
-rw-r--r--src/daemons/server-stats.ts2
-rw-r--r--src/db/postgre.ts4
-rw-r--r--src/docs/fr-FR/api.md80
-rw-r--r--src/docs/fr-FR/create-plugin.md2
-rw-r--r--src/docs/fr-FR/follow.md2
-rw-r--r--src/docs/fr-FR/pages.md4
-rw-r--r--src/docs/fr-FR/stream.md2
-rw-r--r--src/docs/fr-FR/theme.md4
-rw-r--r--src/docs/it-IT/custom-emoji.md4
-rw-r--r--src/docs/it-IT/follow.md4
-rw-r--r--src/docs/it-IT/keyboard-shortcut.md34
-rw-r--r--src/docs/it-IT/mute.md16
-rw-r--r--src/docs/it-IT/pages.md2
-rw-r--r--src/docs/it-IT/reaction.md16
-rw-r--r--src/docs/it-IT/theme.md18
-rw-r--r--src/docs/it-IT/timelines.md12
-rw-r--r--src/global.d.ts1
-rw-r--r--src/misc/before-shutdown.ts90
-rw-r--r--src/misc/cache.ts43
-rw-r--r--src/misc/fetch-meta.ts2
-rw-r--r--src/misc/keypair-store.ts10
-rw-r--r--src/misc/populate-emojis.ts119
-rw-r--r--src/models/entities/drive-file.ts2
-rw-r--r--src/models/entities/note-reaction.ts4
-rw-r--r--src/models/repositories/antenna.ts18
-rw-r--r--src/models/repositories/drive-file.ts36
-rw-r--r--src/models/repositories/note.ts120
-rw-r--r--src/models/repositories/notification.ts72
-rw-r--r--src/models/repositories/user.ts24
-rw-r--r--src/queue/index.ts23
-rw-r--r--src/queue/initialize.ts18
-rw-r--r--src/queue/processors/deliver.ts19
-rw-r--r--src/queue/processors/inbox.ts1
-rw-r--r--src/queue/queues.ts7
-rw-r--r--src/remote/activitypub/deliver-manager.ts6
-rw-r--r--src/remote/activitypub/renderer/index.ts6
-rw-r--r--src/remote/activitypub/renderer/person.ts5
-rw-r--r--src/remote/activitypub/request.ts10
-rw-r--r--src/server/activitypub.ts5
-rw-r--r--src/server/api/authenticate.ts13
-rw-r--r--src/server/api/common/inject-featured.ts2
-rw-r--r--src/server/api/define.ts1
-rw-r--r--src/server/api/endpoints/admin/invite.ts2
-rw-r--r--src/server/api/endpoints/admin/promo/create.ts2
-rw-r--r--src/server/api/endpoints/antennas/notes.ts6
-rw-r--r--src/server/api/endpoints/auth/accept.ts2
-rw-r--r--src/server/api/endpoints/channels/follow.ts5
-rw-r--r--src/server/api/endpoints/channels/timeline.ts6
-rw-r--r--src/server/api/endpoints/channels/unfollow.ts3
-rw-r--r--src/server/api/endpoints/clips/add-note.ts2
-rw-r--r--src/server/api/endpoints/clips/notes.ts6
-rw-r--r--src/server/api/endpoints/i.ts27
-rw-r--r--src/server/api/endpoints/i/notifications.ts10
-rw-r--r--src/server/api/endpoints/i/read-announcement.ts2
-rw-r--r--src/server/api/endpoints/i/update.ts3
-rw-r--r--src/server/api/endpoints/miauth/gen-token.ts2
-rw-r--r--src/server/api/endpoints/mute/create.ts3
-rw-r--r--src/server/api/endpoints/mute/delete.ts3
-rw-r--r--src/server/api/endpoints/notes.ts6
-rw-r--r--src/server/api/endpoints/notes/children.ts6
-rw-r--r--src/server/api/endpoints/notes/favorites/create.ts2
-rw-r--r--src/server/api/endpoints/notes/featured.ts6
-rw-r--r--src/server/api/endpoints/notes/global-timeline.ts11
-rw-r--r--src/server/api/endpoints/notes/hybrid-timeline.ts11
-rw-r--r--src/server/api/endpoints/notes/local-timeline.ts11
-rw-r--r--src/server/api/endpoints/notes/mentions.ts10
-rw-r--r--src/server/api/endpoints/notes/renotes.ts6
-rw-r--r--src/server/api/endpoints/notes/replies.ts6
-rw-r--r--src/server/api/endpoints/notes/search-by-tag.ts6
-rw-r--r--src/server/api/endpoints/notes/search.ts6
-rw-r--r--src/server/api/endpoints/notes/timeline.ts11
-rw-r--r--src/server/api/endpoints/notes/user-list-timeline.ts6
-rw-r--r--src/server/api/endpoints/pages/like.ts2
-rw-r--r--src/server/api/endpoints/promo/read.ts2
-rw-r--r--src/server/api/endpoints/sw/register.ts2
-rw-r--r--src/server/api/endpoints/users/followers.ts3
-rw-r--r--src/server/api/endpoints/users/following.ts3
-rw-r--r--src/server/api/endpoints/users/groups/create.ts2
-rw-r--r--src/server/api/endpoints/users/groups/invitations/accept.ts2
-rw-r--r--src/server/api/endpoints/users/notes.ts6
-rw-r--r--src/server/api/private/signin.ts4
-rw-r--r--src/server/api/stream/channels/antenna.ts2
-rw-r--r--src/server/api/stream/channels/channel.ts2
-rw-r--r--src/server/api/stream/channels/global-timeline.ts2
-rw-r--r--src/server/api/stream/channels/hashtag.ts2
-rw-r--r--src/server/api/stream/channels/home-timeline.ts2
-rw-r--r--src/server/api/stream/channels/hybrid-timeline.ts2
-rw-r--r--src/server/api/stream/channels/local-timeline.ts2
-rw-r--r--src/server/api/stream/channels/main.ts8
-rw-r--r--src/server/api/stream/index.ts112
-rw-r--r--src/server/web/index.ts12
-rw-r--r--src/server/web/views/flush.pug59
-rw-r--r--src/services/add-note-to-antenna.ts2
-rw-r--r--src/services/blocking/create.ts14
-rw-r--r--src/services/chart/charts/classes/active-users.ts18
-rw-r--r--src/services/chart/charts/classes/drive.ts22
-rw-r--r--src/services/chart/charts/classes/federation.ts11
-rw-r--r--src/services/chart/charts/classes/hashtag.ts18
-rw-r--r--src/services/chart/charts/classes/instance.ts44
-rw-r--r--src/services/chart/charts/classes/network.ts11
-rw-r--r--src/services/chart/charts/classes/notes.ts26
-rw-r--r--src/services/chart/charts/classes/per-user-drive.ts12
-rw-r--r--src/services/chart/charts/classes/per-user-following.ts30
-rw-r--r--src/services/chart/charts/classes/per-user-notes.ts14
-rw-r--r--src/services/chart/charts/classes/per-user-reactions.ts12
-rw-r--r--src/services/chart/charts/classes/test-grouped.ts11
-rw-r--r--src/services/chart/charts/classes/test-unique.ts13
-rw-r--r--src/services/chart/charts/classes/test.ts11
-rw-r--r--src/services/chart/charts/classes/users.ts16
-rw-r--r--src/services/chart/charts/schemas/active-users.ts12
-rw-r--r--src/services/chart/charts/schemas/hashtag.ts12
-rw-r--r--src/services/chart/charts/schemas/test-unique.ts7
-rw-r--r--src/services/chart/core.ts276
-rw-r--r--src/services/chart/index.ts25
-rw-r--r--src/services/create-notification.ts2
-rw-r--r--src/services/following/create.ts11
-rw-r--r--src/services/following/delete.ts7
-rw-r--r--src/services/following/requests/reject.ts7
-rw-r--r--src/services/i/pin.ts2
-rw-r--r--src/services/insert-moderation-log.ts2
-rw-r--r--src/services/messages/create.ts6
-rw-r--r--src/services/note/create.ts50
-rw-r--r--src/services/note/polls/vote.ts2
-rw-r--r--src/services/note/reaction/create.ts48
-rw-r--r--src/services/note/read.ts80
-rw-r--r--src/services/note/unread.ts6
-rw-r--r--src/services/note/watch.ts2
-rw-r--r--src/services/register-or-fetch-instance-doc.ts8
-rw-r--r--src/services/stream.ts5
-rw-r--r--src/services/update-hashtag.ts4
-rw-r--r--src/services/user-list/push.ts2
-rw-r--r--src/tsconfig.json39
145 files changed, 1585 insertions, 724 deletions
diff --git a/src/.eslintrc b/src/.eslintrc
new file mode 100644
index 0000000000..d54e20f6b6
--- /dev/null
+++ b/src/.eslintrc
@@ -0,0 +1,6 @@
+{
+ "env": {
+ "node": true,
+ "commonjs": true
+ }
+}
diff --git a/src/client/.eslintrc b/src/client/.eslintrc
index 8829472b49..fffa28d9e4 100644
--- a/src/client/.eslintrc
+++ b/src/client/.eslintrc
@@ -1,4 +1,24 @@
{
+ "env": {
+ "node": false,
+ },
+ "extends": [
+ "eslint:recommended",
+ "plugin:vue/recommended"
+ ],
+ "rules": {
+ "vue/require-v-for-key": 0,
+ "vue/max-attributes-per-line": 0,
+ "vue/html-indent": 0,
+ "vue/html-self-closing": 0,
+ "vue/no-unused-vars": 0,
+ "vue/attributes-order": 0,
+ "vue/require-prop-types": 0,
+ "vue/require-default-prop": 0,
+ "vue/html-closing-bracket-spacing": 0,
+ "vue/singleline-html-element-content-newline": 0,
+ "vue/no-v-html": 0
+ },
"globals": {
"_DEV_": false,
"_LANGS_": false,
diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue
index 1ef3f43389..ea26d31100 100644
--- a/src/client/components/note-detailed.vue
+++ b/src/client/components/note-detailed.vue
@@ -350,7 +350,8 @@ export default defineComponent({
capture(withHandler = false) {
if (this.$i) {
- this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id });
+ // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
+ this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id });
if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
}
},
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index 65e09b7802..70f49fef7e 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -325,7 +325,8 @@ export default defineComponent({
capture(withHandler = false) {
if (this.$i) {
- this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id });
+ // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
+ this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id });
if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
}
},
diff --git a/src/client/components/ui/modal.vue b/src/client/components/ui/modal.vue
index ff5b98d39f..db6564bacc 100644
--- a/src/client/components/ui/modal.vue
+++ b/src/client/components/ui/modal.vue
@@ -1,7 +1,7 @@
<template>
<transition :name="$store.state.animation ? popup ? 'modal-popup' : 'modal' : ''" 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 class="bg _modalBg" @click="onBgClick"></div>
+ <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>
</div>
diff --git a/src/client/init.ts b/src/client/init.ts
index 1c44e7f23e..2a2b6a2f86 100644
--- a/src/client/init.ts
+++ b/src/client/init.ts
@@ -4,40 +4,6 @@
import '@/style.scss';
-// TODO: そのうち消す
-if (localStorage.getItem('vuex') != null) {
- const vuex = JSON.parse(localStorage.getItem('vuex'));
-
- localStorage.setItem('account', JSON.stringify({
- ...vuex.i,
- token: localStorage.getItem('i')
- }));
- localStorage.setItem('accounts', JSON.stringify(vuex.device.accounts));
- localStorage.setItem('miux:themes', JSON.stringify(vuex.device.themes));
-
- if (vuex.device.userData) {
- for (const [k, v] of Object.entries(vuex.device.userData)) {
- localStorage.setItem('pizzax::base::' + k, JSON.stringify({
- widgets: v.widgets
- }));
-
- if (v.deck) {
- localStorage.setItem('pizzax::deck::' + k, JSON.stringify({
- columns: v.deck.columns,
- layout: v.deck.layout,
- }));
- }
- }
- }
-
- localStorage.setItem('vuex-old', JSON.stringify(vuex));
- localStorage.removeItem('vuex');
- localStorage.removeItem('i');
- localStorage.removeItem('locale');
-
- location.reload();
-}
-
import * as Sentry from '@sentry/browser';
import { Integrations } from '@sentry/tracing';
import { createApp, watch } from 'vue';
diff --git a/src/client/pages/doc.vue b/src/client/pages/doc.vue
index 3379a5fe68..ed4eae4d02 100644
--- a/src/client/pages/doc.vue
+++ b/src/client/pages/doc.vue
@@ -60,7 +60,7 @@ export default defineComponent({
methods: {
fetchDoc() {
- fetch(`${url}/assets/docs/${lang}/${this.doc}.md`).then(res => res.text()).then(md => {
+ fetch(`${url}/doc-assets/${lang}/${this.doc}.md`).then(res => res.text()).then(md => {
this.parse(md);
});
},
diff --git a/src/client/pages/page-editor/page-editor.vue b/src/client/pages/page-editor/page-editor.vue
index 45997dfd65..08856ebfe4 100644
--- a/src/client/pages/page-editor/page-editor.vue
+++ b/src/client/pages/page-editor/page-editor.vue
@@ -4,9 +4,9 @@
<MkA class="view" v-if="pageId" :to="`/@${ author.username }/pages/${ currentName }`"><Fa :icon="faExternalLinkSquareAlt"/> {{ $ts._pages.viewPage }}</MkA>
<div class="buttons" style="margin: 16px 0;">
- <MkButton inline @click="save" primary class="save"><Fa :icon="faSave"/> {{ $ts.save }}</MkButton>
+ <MkButton inline @click="save" primary class="save" v-if="!readonly"><Fa :icon="faSave"/> {{ $ts.save }}</MkButton>
<MkButton inline @click="duplicate" class="duplicate" v-if="pageId"><Fa :icon="faCopy"/> {{ $ts.duplicate }}</MkButton>
- <MkButton inline @click="del" class="delete" v-if="pageId"><Fa :icon="faTrashAlt"/> {{ $ts.delete }}</MkButton>
+ <MkButton inline @click="del" class="delete" v-if="pageId && !readonly"><Fa :icon="faTrashAlt"/> {{ $ts.delete }}</MkButton>
</div>
<MkContainer :body-togglable="true" :expanded="true" class="_vMargin">
@@ -134,12 +134,18 @@ export default defineComponent({
data() {
return {
- INFO: computed(() => this.initPageId ? {
- title: this.$ts._pages.editPage,
- icon: faPencilAlt,
- } : {
- title: this.$ts._pages.newPage,
- icon: faPencilAlt,
+ INFO: computed(() => {
+ let title = this.$ts._pages.newPage;
+ if (this.initPageId) {
+ title = this.$ts._pages.editPage;
+ }
+ else if (this.initPageName && this.initUser) {
+ title = this.$ts._pages.readPage;
+ }
+ return {
+ title: title,
+ icon: faPencilAlt,
+ };
}),
author: this.$i,
readonly: false,
diff --git a/src/client/store.ts b/src/client/store.ts
index 14924dadd0..e6fdd12f1d 100644
--- a/src/client/store.ts
+++ b/src/client/store.ts
@@ -212,7 +212,6 @@ type Plugin = {
*/
export class ColdDeviceStorage {
public static default = {
- themes: [] as Theme[], // TODO: そのうち消す
// TODO: テーマをアカウントに保存するようになったのにもかかわらず、以下のどのテーマを使うかという情報だけがブラウザ保存になっていて、アカウント切り替えたりログアウトしたときに不具合が発生するのでなんとかする
// テーマIDを保存するのではなく、テーマ自体を保存するようにすれば解決するかも
darkTheme: '8050783a-7f63-445a-b270-36d0f6ba1677',
diff --git a/src/client/theme-store.ts b/src/client/theme-store.ts
index 5e440efbf9..8e21af70fc 100644
--- a/src/client/theme-store.ts
+++ b/src/client/theme-store.ts
@@ -33,30 +33,3 @@ export async function removeTheme(theme: Theme): Promise<void> {
await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
localStorage.setItem(lsCacheKey, JSON.stringify(themes));
}
-
-// TODO: そのうち消す
-if (ColdDeviceStorage.get('themes').length > 0) {
- const lsThemes = ColdDeviceStorage.get('themes');
- let registryThemes;
- try {
- registryThemes = await api('i/registry/get', { scope: ['client'], key: 'themes' });
- } catch (e) {
- if (e.code === 'NO_SUCH_KEY') {
- registryThemes = [];
- } else {
- throw e;
- }
- }
- const themes = [] as Theme[];
- for (const theme of lsThemes) {
- if (themes.some(x => x.id === theme.id)) continue;
- themes.push(theme);
- }
- for (const theme of registryThemes) {
- if (themes.some(x => x.id === theme.id)) continue;
- themes.push(theme);
- }
- await api('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
- localStorage.setItem(lsCacheKey, JSON.stringify(themes));
- ColdDeviceStorage.set('themes', []);
-}
diff --git a/src/client/ui/chat/note.vue b/src/client/ui/chat/note.vue
index 5a4a13d889..97275875ca 100644
--- a/src/client/ui/chat/note.vue
+++ b/src/client/ui/chat/note.vue
@@ -325,7 +325,8 @@ export default defineComponent({
capture(withHandler = false) {
if (this.$i) {
- this.connection.send(document.body.contains(this.$el) ? 'sn' : 's', { id: this.appearNote.id });
+ // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
+ this.connection.send(document.body.contains(this.$el) ? 'sr' : 's', { id: this.appearNote.id });
if (withHandler) this.connection.on('noteUpdated', this.onStreamNoteUpdated);
}
},
diff --git a/src/daemons/janitor.ts b/src/daemons/janitor.ts
index 462ebf915c..c079086427 100644
--- a/src/daemons/janitor.ts
+++ b/src/daemons/janitor.ts
@@ -1,3 +1,5 @@
+// TODO: 消したい
+
const interval = 30 * 60 * 1000;
import { AttestationChallenges } from '../models';
import { LessThan } from 'typeorm';
diff --git a/src/daemons/queue-stats.ts b/src/daemons/queue-stats.ts
index 288e855ae9..77f09b18d6 100644
--- a/src/daemons/queue-stats.ts
+++ b/src/daemons/queue-stats.ts
@@ -1,5 +1,5 @@
import Xev from 'xev';
-import { deliverQueue, inboxQueue } from '../queue';
+import { deliverQueue, inboxQueue } from '../queue/queues';
const ev = new Xev();
diff --git a/src/daemons/server-stats.ts b/src/daemons/server-stats.ts
index bc1da5eef6..8dfa946250 100644
--- a/src/daemons/server-stats.ts
+++ b/src/daemons/server-stats.ts
@@ -75,5 +75,5 @@ async function net() {
// FS STAT
async function fs() {
const data = await si.disksIO().catch(() => ({ rIO_sec: 0, wIO_sec: 0 }));
- return data;
+ return data || { rIO_sec: 0, wIO_sec: 0 };
}
diff --git a/src/db/postgre.ts b/src/db/postgre.ts
index 2f3c910163..831e5e0592 100644
--- a/src/db/postgre.ts
+++ b/src/db/postgre.ts
@@ -1,3 +1,7 @@
+// https://github.com/typeorm/typeorm/issues/2400
+const types = require('pg').types;
+types.setTypeParser(20, Number);
+
import { createConnection, Logger, getConnection } from 'typeorm';
import config from '../config';
import { entities as charts } from '../services/chart/entities';
diff --git a/src/docs/fr-FR/api.md b/src/docs/fr-FR/api.md
index 76019b6145..0147f95bac 100644
--- a/src/docs/fr-FR/api.md
+++ b/src/docs/fr-FR/api.md
@@ -1,58 +1,58 @@
-# Misskey API
+# API de Misskey
-MisskeyAPIを使ってMisskeyクライアント、Misskey連携Webサービス、Bot等(以下「アプリケーション」と呼びます)を開発できます。 ストリーミングAPIもあるので、リアルタイム性のあるアプリケーションを作ることも可能です。
+Vous pouvez utiliser l'API de Misskey pour développer des clients Misskey, des services web s'intégrant à Misskey, des Bots (que nous appellerons plus loin "Applications"), etc. Comme l'API streaming est aussi implémenté, vous avez la possibilité de créer des applications de temps réel.
-APIを使い始めるには、まずアクセストークンを取得する必要があります。 このドキュメントでは、アクセストークンを取得する手順を説明した後、基本的なAPIの使い方を説明します。
+Pour pouvoir vous servir de l'API, il vous faudra d'abord obtenir un jeton d'accès. Ce guide a été conçu pour vous accompagner dans le processus d'obtention du jeton d'accès, puis donner des instructions de base sur l'utilisation de l'API.
-## アクセストークンの取得
-基本的に、APIはリクエストにはアクセストークンが必要となります。 APIにリクエストするのが自分自身なのか、不特定の利用者に使ってもらうアプリケーションなのかによって取得手順は異なります。
+## Obtenir le jeton d'accès
+Une requête d'API, par essence, nécessite un jeton d'accès. La procédure d'acquisition du jeton diffère selon que vous effectuez la requête vous-même, ou qu'elle est envoyée via une application par un utilisateur final non défini.
-* 前者の場合: [「自分自身のアクセストークンを手動発行する」](#自分自身のアクセストークンを手動発行する)に進む
-* 後者の場合: [「アプリケーション利用者にアクセストークンの発行をリクエストする」](#アプリケーション利用者にアクセストークンの発行をリクエストする)に進む
+* Dans le premier cas : allez à [« Générer manuellement un jeton d'accès pour son propre compte »](#自分自身のアクセストークンを手動発行する).
+* Dans le second cas : allez à [« Demander la génération du jeton d'accès via un utilisateur d'application »](#アプリケーション利用者にアクセストークンの発行をリクエストする).
-### 自分自身のアクセストークンを手動発行する
-「設定 > API」で、自分のアクセストークンを発行できます。
+### Générer manuellement un jeton d'accès pour son propre compte
+Vous pouvez générer votre propre jeton d'accès en allant dans { Paramètres > API }.
-[「APIの使い方」へ進む](#APIの使い方)
+[Continuer avec « Utiliser l'API ».](#APIの使い方)
-### アプリケーション利用者にアクセストークンの発行をリクエストする
-アプリケーション利用者のアクセストークンを取得するには、以下の手順で発行をリクエストします。
+### Demander la génération du jeton d'accès via un utilisateur d'application
+Pour obtenir un jeton d'accès pour le compte utilisateur final de votre application, suivez la procédure de génération ci-dessous.
-#### Step 1
+#### Étape 1
-UUIDを生成する。以後これをセッションIDと呼びます。
+Générez un UUID. Nous l'appellerons « ID de session » dans la suite de ce guide.
-> このセッションIDは毎回生成し、使いまわさないようにしてください。
+> Un même ID de session ne devrait pas être utilisé plusieurs fois ; veillez à en générer un nouveau pour chaque jeton d'accès.
-#### Step 2
+#### Étape 2
-`{_URL_}/miauth/{session}`をユーザーのブラウザで表示させる。`{session}`の部分は、セッションIDに置き換えてください。
-> 例: `{_URL_}/miauth/c1f6d42b-468b-4fd2-8274-e58abdedef6f`
+Ouvrez l'adresse `{_URL_}/miauth/{session}` dans le navigateur de l'utilisateur. Remplacez alors la partie `{session}` de l'URL par l'ID de session que vous venez de générer.
+> Par ex. : `{_URL_}/miauth/c1f6d42b-468b-4fd2-8274-e58abdedef6f`
-表示する際、URLにクエリパラメータとしていくつかのオプションを設定できます:
-* `name` ... アプリケーション名
- * > 例: `MissDeck`
-* `icon` ... アプリケーションのアイコン画像URL
- * > 例: `https://missdeck.example.com/icon.png`
-* `callback` ... 認証が終わった後にリダイレクトするURL
- * > 例: `https://missdeck.example.com/callback`
- * リダイレクト時には、`session`というクエリパラメータでセッションIDが付きます
-* `permission` ... アプリケーションが要求する権限
- * > 例: `write:notes,write:following,read:drive`
- * 要求する権限を`,`で区切って列挙します
- * どのような権限があるかは[APIリファレンス](/api-doc)で確認できます
+En ouvrant cette URL, vous pourrez configurer un certain nombre d'options pour les paramètres de requête :
+* `name` : nom de l'application
+ * > Ex. : `MissDeck`
+* `icon` : URL de l'icône de l'application
+ * > Ex. : `https://missdeck.example.com/icon.png`
+* `callback` : URL de redirection après l'authentification
+ * > Ex. : `https://missdeck.example.com/callback`
+ * Lors de la redirection, un paramètre de requête `session` contenant l'ID de session sera joint.
+* `permission` : permissions requises par l'application
+ * > Ex. : `write:notes,write:following,read:drive`
+ * Listez les permissions requises en utilisant une `,` pour les séparer.
+ * Vous pouvez vérifier quelles sont les permissions disponibles sur [les références API de Misskey](/api-doc).
-#### Step 3
-ユーザーが発行を許可した後、`{_URL_}/api/miauth/{session}/check`にPOSTリクエストすると、レスポンスとしてアクセストークンを含むJSONが返ります。
+#### Étape 3
+Si vous envoyez une requête POST à `{_URL_}/api/miauth/{session}/check` une fois que l'utilisateur a validé le jeton d'accès, la réponse arrivera sous forme de fichier JSON contenant ce jeton.
-レスポンスに含まれるプロパティ:
-* `token` ... ユーザーのアクセストークン
-* `user` ... ユーザーの情報
+Propriétés incluses dans la réponse :
+* `token` : jeton d'accès de l'utilisateur
+* `user` : données de l'utilisateur
-[「APIの使い方」へ進む](#APIの使い方)
+[Continuer avec « Utiliser l'API ».](#APIの使い方)
-## APIの使い方
-**APIはすべてPOSTで、リクエスト/レスポンスともにJSON形式です。RESTではありません。** アクセストークンは、`i`というパラメータ名でリクエストに含めます。
+## Utiliser l'API
+**L'API utilise seulement la méthode POST, et toutes les requêtes / réponses sont au format JSON. REST n'est pas pris en charge. ** Le jeton d'accès s'insère dans le paramètre de requête nommé `i`.
-* [APIリファレンス](/api-doc)
-* [ストリーミングAPI](./stream)
+* [Références API de Misskey](/api-doc)
+* [API streaming](./stream)
diff --git a/src/docs/fr-FR/create-plugin.md b/src/docs/fr-FR/create-plugin.md
index 540909de42..c0d5fa9c57 100644
--- a/src/docs/fr-FR/create-plugin.md
+++ b/src/docs/fr-FR/create-plugin.md
@@ -34,7 +34,7 @@ Misskey Webクライアントのプラグイン機能を使うと、クライア
#### default
設定のデフォルト値
-## APIリファレンス
+## Références API de Misskey
AiScript標準で組み込まれているAPIは掲載しません。
### Mk:dialog(title text type)
diff --git a/src/docs/fr-FR/follow.md b/src/docs/fr-FR/follow.md
index 5ef74c0903..fde5c5e471 100644
--- a/src/docs/fr-FR/follow.md
+++ b/src/docs/fr-FR/follow.md
@@ -1,2 +1,2 @@
# Abonnements
-Lorsque vous suivez un·e utilisateur·rice, ses publications apparaissent dans votre fil.Cela n'inclut toutefois pas ses réponses aux autres utilisateur·ice·s. Vous pouvez vous désabonner du compte en cliquant une seconde fois.
+Lorsque vous suivez un·e utilisateur·rice, ses publications apparaissent dans votre fil.Cela n'inclut toutefois pas ses réponses aux autres utilisateur·ice·s. Pour suivre un compte, rendez-vous sur sa page et cliquez sur le bouton « s'abonner ». Vous pouvez vous désabonner du compte en cliquant une seconde fois.
diff --git a/src/docs/fr-FR/pages.md b/src/docs/fr-FR/pages.md
index 9021d41b1c..0dbafb806e 100644
--- a/src/docs/fr-FR/pages.md
+++ b/src/docs/fr-FR/pages.md
@@ -3,8 +3,8 @@
## Variables
Vous pouvez créer des pages dynamiques en utilisant des variables.Vous pouvez incorporer la valeur d'une variable en insérant le <b>{ variablename }</b> dans votre texte.Par exemple, si la valeur de la variable "thing" dans le texte <b>Hello { thing } world!</b> est <b>ai</b>, votre trexte devient alors : <b>Hello ai world!</b>.
-Les variables sont évaluées du haut vers le bas, il n'est donc pas possible de référencer une variable située plus bas que celle en cours.Par exemple, si vous définissez, dans l'ordre, 3 variables telles que <b>A、B、C</b>, vous pourrez référencer en <b>C</b> aussi bien <b>A</b> que <b>B</b> ; par contre, vous ne pourrez référencer en <b>A</b> ni <b>B</b> ni <b>C</b>.
+Les variables sont prises en compte dans l'ordre chronologique, de haut en bas. Il n'est donc pas possible d'appeler une variable située plus bas dans le code. Par exemple, si vous définissez, dans l'ordre, 3 variables telles que <b>A, B, C</b>, vous pourrez appeler en <b>C</b> aussi bien <b>A</b> que <b>B</b> ; par contre, vous ne pourrez appeler en <b>A</b> ni <b>B</b> ni <b>C</b>.
Pour recevoir une entrée utilisateur, ajoutez un bloc "Entrée" sur la page et définissez le nom des variables que vous souhaitez stocker dans le champ "Nom de la variable" (les variables seront créées automatiquement).Vous pourrez alors exécuter les actions en fonction de l'entrée utilisateur de ces variables.
-Utiliser des fonctions vous permettra de mettre en place une façon de calculer des valeurs que vous pourrez réutiliser.Pour créer des fonctions, il faut d'abord définir une variable du type "fonction".Ensuite, vous pouvez configurer des arguments dont la valeur sera utilisable comme une variable à l'intérieur de la fonction. Par ailleurs, il existe ce que l'on appelle des "fonctions d'ordre supérieur" dont les arguments sont aussi des fonctions. En plus de paramétrer des fonctions à l'avance, vous avez également la possibilité de définir des fonctions à l'improviste directement dans les arguments de ces "fonctions d'ordre supérieur".
+Appeler des fonctions vous permet de définir des valeurs que vous pourrez réutiliser. Pour créer des fonctions, il faut d'abord définir une variable du type "fonction". Vous pouvez y configurer des « slots » (arguments), dont la valeur devient alors disponible en tant que variable à l'intérieur de la fonction. Par ailleurs, il existe ce que l'on appelle des "fonctions d'ordre supérieur" dont les arguments sont aussi des fonctions. En plus de paramétrer des fonctions à l'avance, vous avez également la possibilité de définir des fonctions à l'improviste directement dans les « slots » de ces fonctions d'ordre supérieur.
diff --git a/src/docs/fr-FR/stream.md b/src/docs/fr-FR/stream.md
index 04dad66799..55e6bc6806 100644
--- a/src/docs/fr-FR/stream.md
+++ b/src/docs/fr-FR/stream.md
@@ -1,4 +1,4 @@
-# ストリーミングAPI
+# API streaming
ストリーミングAPIを使うと、リアルタイムで様々な情報(例えばタイムラインに新しい投稿が流れてきた、メッセージが届いた、フォローされた、など)を受け取ったり、様々な操作を行ったりすることができます。
diff --git a/src/docs/fr-FR/theme.md b/src/docs/fr-FR/theme.md
index 72c60f1c6b..cf15c921e4 100644
--- a/src/docs/fr-FR/theme.md
+++ b/src/docs/fr-FR/theme.md
@@ -6,7 +6,7 @@ Vous pouvez modifier l'apparence de votre client Misskey à l'aide de thèmes.
Paramètres > Thèmes
## Créer un thème
-Les codes des thèmes sont écrits sous forme d'objets JSON5. Les thèmes comprennent les objets suivants :
+Le code des thèmes est écrit sous forme d'objets JSON5. Les thèmes comprennent les objets suivants :
``` js
{
id: '17587283-dd92-4a2c-a22c-be0637c9e22a',
@@ -43,7 +43,7 @@ Les codes des thèmes sont écrits sous forme d'objets JSON5. Les thèmes compre
* `props` ... Définir un style de thème.Voir les explications ci-après.
### Définir un style de thème
-C'est dans `props` que vous définirez le style de thème. Les propriétés deviendront des variables CSS et les valeurs spécifieront le contenu. Par ailleurs, les objets présents par défaut dans `props` sont hérités du thème de base. Ainsi, si le thème de `base` est clair `light` ce sera l'objet [_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5) ; et s'il est sombre `dark` ce sera l'objet [_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5). Cela signifie, par exemple, que s'il n'y pas de propriété `panel` définie dans les `props` du thème, alors ce sera la valeur `panel` du thème de base qui sera prise en compte.
+C'est dans `props` que vous définirez le style de thème. Les propriétés deviendront des variables CSS et les valeurs associées spécifieront le contenu de ces variables. Par ailleurs, les objets présents par défaut dans `props` sont hérités du thème de base. Ainsi, si le thème de `base` est clair `light` ce sera l'objet [_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5) ; et s'il est sombre `dark` ce sera l'objet [_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5). Cela signifie, par exemple, que s'il n'y pas de propriété `panel` définie dans les `props` du thème, alors ce sera la valeur `panel` du thème de base qui sera prise en compte.
#### Syntaxe des valeurs
* Codes de couleur Hex
diff --git a/src/docs/it-IT/custom-emoji.md b/src/docs/it-IT/custom-emoji.md
index ed2e92be16..900f115d3c 100644
--- a/src/docs/it-IT/custom-emoji.md
+++ b/src/docs/it-IT/custom-emoji.md
@@ -1,2 +1,2 @@
-# カスタム絵文字
-カスタム絵文字は、インスタンスで用意された画像を絵文字のように使える機能です。 ノート、リアクション、チャット、自己紹介、名前などの場所で使うことができます。 カスタム絵文字をそれらの場所で使うには、絵文字ピッカーボタン(ある場合)を押すか、`:`を入力して絵文字サジェストを表示します。 テキスト内に`:foo:`のような形式の文字列が見つかると、`foo`の部分がカスタム絵文字名と解釈され、表示時には対応したカスタム絵文字に置き換わります。
+# Emoji personalizzati
+Gli emoji personalizzati sono una funzionalità che ti permette di usare delle immagini preparate dalla tua istanza come emoji. Si possono usare in note, reazioni, chat, nella biografia di profilo, nel tuo nome, e altrove su Misskey. Per usare gli emoji personalizzati, puoi aprire la tastiera emoji (quando c'è), oppure visualizzare suggerimenti emoji scrivendo `:`. Quando una sequenza di caratteri del tipo `:foo:` è trovata in un testo, la parte centrale `foo` viene interpretata come un nome di emoji personalizzato e quindi viene sostituita dall'emoji corrispondente.
diff --git a/src/docs/it-IT/follow.md b/src/docs/it-IT/follow.md
index f636a710f3..c54099a361 100644
--- a/src/docs/it-IT/follow.md
+++ b/src/docs/it-IT/follow.md
@@ -1,2 +1,2 @@
-# Seiguiti
-ユーザーをフォローすると、タイムラインにそのユーザーの投稿が表示されるようになります。ただし、他のユーザーに対する返信は含まれません。 ユーザーをフォローするには、ユーザーページの「フォロー」ボタンをクリックします。フォローを解除するには、もう一度クリックします。
+# Follow
+Se segui un utente, le sue pubblicazioni verranno mostrate sulla tua timeline. A esclusione delle sue risposte ad altrə utenti. Per seguire un utente, bisogna premere il pulsante "seguire" della sua pagina. Premi una seconda volta sul pulsante per smettere di seguire l'account.
diff --git a/src/docs/it-IT/keyboard-shortcut.md b/src/docs/it-IT/keyboard-shortcut.md
index a547e6115e..c9bb815bae 100644
--- a/src/docs/it-IT/keyboard-shortcut.md
+++ b/src/docs/it-IT/keyboard-shortcut.md
@@ -1,17 +1,17 @@
-# キーボードショートカット
+# Scorciatoie da tastiera
-## グローバル
-これらのショートカットは基本的にどこでも使えます。
+## Generali
+Le scorciatoie da tastiera sotto citate si possono usare praticamente ovunque.
<table>
<thead>
- <tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
+ <tr><th>Scorciatoia</th><th>Effetto</th><th>Accesso universale</th></tr>
</thead>
<tbody>
- <tr><td><kbd class="key">P</kbd>, <kbd class="key">N</kbd></td><td>新規投稿</td><td><b>P</b>ost, <b>N</b>ew, <b>N</b>ote</td></tr>
- <tr><td><kbd class="key">T</kbd></td><td>タイムラインの最も新しい投稿にフォーカス</td><td><b>T</b>imeline, <b>T</b>op</td></tr>
- <tr><td><kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">N</kbd></kbd></td><td>通知を表示/隠す</td><td><b>N</b>otifications</td></tr>
+ <tr><td><kbd class="key">P</kbd>, <kbd class="key">N</kbd></td><td>Nuova pubblicazione</td><td><b>P</b>ost, <b>N</b>ew, <b>N</b>ote</td></tr>
+ <tr><td><kbd class="key">T</kbd></td><td>Evidenziare l'ultima pubblicazione sulla timeline</td><td><b>T</b>imeline, <b>T</b>op</td></tr>
+ <tr><td><kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">N</kbd></kbd></td><td>Mostrare/nascondere notifiche</td><td><b>N</b>otifications</td></tr>
<tr><td><kbd class="key">S</kbd></td><td>Cerca</td><td><b>S</b>earch</td></tr>
- <tr><td><kbd class="key">H</kbd>, <kbd class="key">?</kbd></td><td>ヘルプを表示</td><td><b>H</b>elp</td></tr>
+ <tr><td><kbd class="key">H</kbd>, <kbd class="key">?</kbd></td><td>Visualizzare l'aiuto</td><td><b>H</b>elp</td></tr>
</tbody>
</table>
@@ -19,7 +19,7 @@
<table>
<thead>
- <tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
+ <tr><th>Scorciatoia</th><th>Effetto</th><th>Accesso universale</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key">↑</kbd>, <kbd class="key">K</kbd>, <kbd class="group"><kbd class="key">Shift</kbd> + <kbd class="key">Tab</kbd></kbd></td><td>上の投稿にフォーカスを移動</td><td>-</td></tr>
@@ -37,24 +37,24 @@
</tbody>
</table>
-## Renoteフォーム
+## Finestra Rinota
<table>
<thead>
- <tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
+ <tr><th>Scorciatoia</th><th>Effetto</th><th>Accesso universale</th></tr>
</thead>
<tbody>
- <tr><td><kbd class="key">Enter</kbd></td><td>Renoteする</td><td>-</td></tr>
- <tr><td><kbd class="key">Q</kbd></td><td>フォームを展開する</td><td><b>Q</b>uote</td></tr>
- <tr><td><kbd class="key">Esc</kbd></td><td>フォームを閉じる</td><td>-</td></tr>
+ <tr><td><kbd class="key">Enter</kbd></td><td>Rinotare</td><td>-</td></tr>
+ <tr><td><kbd class="key">Q</kbd></td><td>Aprire finestra</td><td><b>Q</b>uote</td></tr>
+ <tr><td><kbd class="key">Esc</kbd></td><td>Chiudere finestra</td><td>-</td></tr>
</tbody>
</table>
-## リアクションフォーム
-デフォルトで「👍」にフォーカスが当たっている状態です。
+## Pannello reazioni
+La reazione "👍" è impostata come reazione predefinita.
<table>
<thead>
- <tr><th>ショートカット</th><th>効果</th><th>由来</th></tr>
+ <tr><th>Scorciatoia</th><th>Effetto</th><th>Accesso universale</th></tr>
</thead>
<tbody>
<tr><td><kbd class="key">↑</kbd>, <kbd class="key">K</kbd></td><td>上のリアクションにフォーカスを移動</td><td>-</td></tr>
diff --git a/src/docs/it-IT/mute.md b/src/docs/it-IT/mute.md
index a6cb073755..d0fa672f4a 100644
--- a/src/docs/it-IT/mute.md
+++ b/src/docs/it-IT/mute.md
@@ -1,13 +1,13 @@
-# Silenzia
+# Silenziare
-ユーザーをミュートすると、そのユーザーに関する次のコンテンツがMisskeyに表示されなくなります:
+Quando si silenzia un utente, i successivi contenuti che lo riguardano non saranno più visualizzati su Misskey:
-* タイムラインや投稿の検索結果内の、そのユーザーの投稿(およびそれらの投稿に対する返信やRenote)
-* そのユーザーからの通知
-* メッセージ履歴一覧内の、そのユーザーとのメッセージ履歴
+* le pubblicazioni dell'utente sia nelle timeline che nei risultati di ricerca, così come le sue risposte e Rinote;
+* le notifiche riguardo all'utente;
+* la cronologia dei messaggi scambiati con l'utente nella chat.
-ユーザーをミュートするには、対象のユーザーのユーザーページに表示されている「ミュート」ボタンを押します。
+Per silenziare un utente, premi il pulsante "Silenzia" che si trova sulla sua pagina profilo.
-ミュートを行ったことは相手に通知されず、ミュートされていることを知ることもできません。
+Gli utenti silenziati da te non vengono informati; nello stesso modo, tu non sarai infomat@ se vieni silenziat@ da un altr@ utente.
-設定>ミュート から、自分がミュートしているユーザー一覧を確認することができます。
+Puoi controllare la lista di utenti che hai silenziato nelle Impostazioni account > "Silenziati / Bloccati".
diff --git a/src/docs/it-IT/pages.md b/src/docs/it-IT/pages.md
index 81c19dd20a..5a34971f1a 100644
--- a/src/docs/it-IT/pages.md
+++ b/src/docs/it-IT/pages.md
@@ -1,4 +1,4 @@
-# Pages
+# Pagine
## Variabili
変数を使うことで動的なページを作成できます。テキスト内で <b>{ 変数名 }</b> と書くとそこに変数の値を埋め込めます。例えば <b>Hello { thing } world!</b> というテキストで、変数(thing)の値が <b>ai</b> だった場合、テキストは <b>Hello ai world!</b> になります。
diff --git a/src/docs/it-IT/reaction.md b/src/docs/it-IT/reaction.md
index eac6cc88e9..f88dd99c3e 100644
--- a/src/docs/it-IT/reaction.md
+++ b/src/docs/it-IT/reaction.md
@@ -1,11 +1,11 @@
-# Reazione
-他の人のノートに、絵文字を付けて簡単にあなたの反応を伝えられる機能です。 リアクションするには、ノートの + アイコンをクリックしてピッカーを表示し、絵文字を選択します。 リアクションには[カスタム絵文字](./custom-emoji)も使用できます。
+# Reazioni
+Puoi mandare una reazione rapida alle note degli/delle altrə utenti apponendoci emoji. Per reagire, premi il pulsante "+" della nota per aprire il pannello reazioni e scegliere un emoji. Si possono anche usare gli [emoji personalizzati](./custom-emoji).
-## リアクションピッカーのカスタマイズ
-ピッカーに表示される絵文字を自分好みにカスタマイズすることができます。 設定の「リアクション」で設定します。
+## Personalizzare il pannello reazioni
+È possibile personalizzare il pannello reazioni selezionando gli emoji che vuoi usare. Puoi cambiare i predefiniti nella scheda "Reazioni" delle impostazioni.
-## リモート投稿へのリアクションについて
-リアクションはMisskeyオリジナルの機能であるため、リモートインスタンスがMisskeyでない限りは、ほとんどの場合「Like」としてアクティビティが送信されます。一般的にはLikeは「お気に入り」として実装されているようです。 また、相手がMisskeyであったとしても、カスタム絵文字リアクションは伝わらず、自動的に「👍」等にフォールバックされます。
+## Inviare reazioni a server remoti
+Siccome le reazioni sono una funzionalità originale di Misskey, vengono ricevute come semplici "Mi piace" dalla maggior parte delle istanze remote del Fediverso, a meno che non sia un'altra istanza Misskey.In genere sul Fediverso, la funzionalità "Mi piace" viene implementata come funzione "Preferiti". Inoltre, se reagisci con un emoji personalizzato verrà automaticamente inoltrato come un "👍" o simile, anche se quella destinataria è un'istanza Misskey.
-## リモートからのリアクションについて
-リモートから「Like」アクティビティを受信したとき、Misskeyでは「👍」のリアクションとして解釈されます。
+## Ricevere reazioni da server remoti
+I "Mi piace" ricevuti da utenti di istanze remote vengono interpretati su Misskey come reazioni a forma di "👍".
diff --git a/src/docs/it-IT/theme.md b/src/docs/it-IT/theme.md
index b877bcd467..ad21299e89 100644
--- a/src/docs/it-IT/theme.md
+++ b/src/docs/it-IT/theme.md
@@ -1,12 +1,12 @@
# Tema
-テーマを設定して、Misskeyクライアントの見た目を変更できます。
+Puoi utilizzare i temi per cambiare l'aspetto del client Misskey.
-## テーマの設定
-設定 > テーマ
+## Impostazioni tema
+Impostazioni > Tema
-## テーマを作成する
-テーマコードはJSON5で記述されたテーマオブジェクトです。 テーマは以下のようなオブジェクトです。
+## Creare un tema
+Il codice dei temi è scritto a forma di oggetti JSON5. I temi contengono gli oggetti sotto citati:
``` js
{
id: '17587283-dd92-4a2c-a22c-be0637c9e22a',
@@ -42,10 +42,10 @@
* テーマはここで設定されたベーステーマを継承します。
* `props` ... テーマのスタイル定義。これから説明します。
-### テーマのスタイル定義
+### Impostare uno stile di tema
`props`下にはテーマのスタイルを定義します。 キーがCSSの変数名になり、バリューで中身を指定します。 なお、この`props`オブジェクトはベーステーマから継承されます。 ベーステーマは、このテーマの`base`が`light`なら[_light.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_light.json5)で、`dark`なら[_dark.json5](https://github.com/syuilo/misskey/blob/develop/src/client/themes/_dark.json5)です。 つまり、このテーマ内の`props`に`panel`というキーが無くても、そこにはベーステーマの`panel`があると見なされます。
-#### バリューで使える構文
+#### Sintassi dei valori
* 16進数で表された色
* 例: `#00ff00`
* `rgb(r, g, b)`形式で表された色
@@ -61,8 +61,8 @@
* 関数(後述)
* `:{関数名}<{引数}<{色}`
-#### Costante
+#### Costanti
「CSS変数として出力はしたくないが、他のCSS変数の値として使いまわしたい」値があるときは、定数を使うと便利です。 キー名を`$`で始めると、そのキーはCSS変数として出力されません。
-#### Funzione
+#### Funzioni
wip
diff --git a/src/docs/it-IT/timelines.md b/src/docs/it-IT/timelines.md
index eb2bb65cad..bd462541d2 100644
--- a/src/docs/it-IT/timelines.md
+++ b/src/docs/it-IT/timelines.md
@@ -1,15 +1,15 @@
-# タイムラインの比較
+# Confronto delle timeline
https://docs.google.com/spreadsheets/d/1lxQ2ugKrhz58Bg96HTDK_2F98BUritkMyIiBkOByjHA/edit?usp=sharing
## Home
-自分のフォローしているユーザーの投稿
+Pubblicazioni degli utenti che segui.
## Locale
-全てのローカルユーザーの「ホーム」指定されていない投稿
+Pubblicazioni degli utenti della tua istanza. Non vengono mostrate le note pubblicate con lo stato "principale".
-## ソーシャル
-自分のフォローしているユーザーの投稿と、全てのローカルユーザーの「ホーム」指定されていない投稿
+## Sociale
+Raggruppa le timeline "home" e "locale".
## グローバル
-全てのローカルユーザーの「ホーム」指定されていない投稿と、サーバーに届いた全てのリモートユーザーの「ホーム」指定されていない投稿
+Tutte le pubblicazioni ricevute dall'istanza, sia locali che altre. Non vengono mostrate le note pubblicate con lo stato "home".
diff --git a/src/global.d.ts b/src/global.d.ts
new file mode 100644
index 0000000000..7343aa1994
--- /dev/null
+++ b/src/global.d.ts
@@ -0,0 +1 @@
+type FIXME = any;
diff --git a/src/misc/before-shutdown.ts b/src/misc/before-shutdown.ts
new file mode 100644
index 0000000000..8639d42b04
--- /dev/null
+++ b/src/misc/before-shutdown.ts
@@ -0,0 +1,90 @@
+// https://gist.github.com/nfantone/1eaa803772025df69d07f4dbf5df7e58
+
+'use strict';
+
+/**
+ * @callback BeforeShutdownListener
+ * @param {string} [signalOrEvent] The exit signal or event name received on the process.
+ */
+
+/**
+ * System signals the app will listen to initiate shutdown.
+ * @const {string[]}
+ */
+const SHUTDOWN_SIGNALS = ['SIGINT', 'SIGTERM'];
+
+/**
+ * Time in milliseconds to wait before forcing shutdown.
+ * @const {number}
+ */
+const SHUTDOWN_TIMEOUT = 15000;
+
+/**
+ * A queue of listener callbacks to execute before shutting
+ * down the process.
+ * @type {BeforeShutdownListener[]}
+ */
+const shutdownListeners = [];
+
+/**
+ * Listen for signals and execute given `fn` function once.
+ * @param {string[]} signals System signals to listen to.
+ * @param {function(string)} fn Function to execute on shutdown.
+ */
+const processOnce = (signals, fn) => {
+ for (const sig of signals) {
+ process.once(sig, fn);
+ }
+};
+
+/**
+ * Sets a forced shutdown mechanism that will exit the process after `timeout` milliseconds.
+ * @param {number} timeout Time to wait before forcing shutdown (milliseconds)
+ */
+const forceExitAfter = timeout => () => {
+ setTimeout(() => {
+ // Force shutdown after timeout
+ console.warn(`Could not close resources gracefully after ${timeout}ms: forcing shutdown`);
+ return process.exit(1);
+ }, timeout).unref();
+};
+
+/**
+ * Main process shutdown handler. Will invoke every previously registered async shutdown listener
+ * in the queue and exit with a code of `0`. Any `Promise` rejections from any listener will
+ * be logged out as a warning, but won't prevent other callbacks from executing.
+ * @param {string} signalOrEvent The exit signal or event name received on the process.
+ */
+async function shutdownHandler(signalOrEvent) {
+ console.warn(`Shutting down: received [${signalOrEvent}] signal`);
+
+ for (const listener of shutdownListeners) {
+ try {
+ await listener(signalOrEvent);
+ } catch (err) {
+ console.warn(`A shutdown handler failed before completing with: ${err.message || err}`);
+ }
+ }
+
+ return process.exit(0);
+}
+
+/**
+ * Registers a new shutdown listener to be invoked before exiting
+ * the main process. Listener handlers are guaranteed to be called in the order
+ * they were registered.
+ * @param {BeforeShutdownListener} listener The shutdown listener to register.
+ * @returns {BeforeShutdownListener} Echoes back the supplied `listener`.
+ */
+export function beforeShutdown(listener) {
+ shutdownListeners.push(listener);
+ return listener;
+}
+
+// Register shutdown callback that kills the process after `SHUTDOWN_TIMEOUT` milliseconds
+// This prevents custom shutdown handlers from hanging the process indefinitely
+processOnce(SHUTDOWN_SIGNALS, forceExitAfter(SHUTDOWN_TIMEOUT));
+
+// Register process shutdown callback
+// Will listen to incoming signal events and execute all registered handlers in the stack
+processOnce(SHUTDOWN_SIGNALS, shutdownHandler);
diff --git a/src/misc/cache.ts b/src/misc/cache.ts
new file mode 100644
index 0000000000..71fbbd8a4c
--- /dev/null
+++ b/src/misc/cache.ts
@@ -0,0 +1,43 @@
+export class Cache<T> {
+ private cache: Map<string | null, { date: number; value: T; }>;
+ private lifetime: number;
+
+ constructor(lifetime: Cache<never>['lifetime']) {
+ this.cache = new Map();
+ this.lifetime = lifetime;
+ }
+
+ public set(key: string | null, value: T): void {
+ this.cache.set(key, {
+ date: Date.now(),
+ value
+ });
+ }
+
+ public get(key: string | null): T | undefined {
+ const cached = this.cache.get(key);
+ if (cached == null) return undefined;
+ if ((Date.now() - cached.date) > this.lifetime) {
+ this.cache.delete(key);
+ return undefined;
+ }
+ return cached.value;
+ }
+
+ public delete(key: string | null) {
+ this.cache.delete(key);
+ }
+
+ public async fetch(key: string | null, fetcher: () => Promise<T>): Promise<T> {
+ const cachedValue = this.get(key);
+ if (cachedValue !== undefined) {
+ // Cache HIT
+ return cachedValue;
+ }
+
+ // Cache MISS
+ const value = await fetcher();
+ this.set(key, value);
+ return value;
+ }
+}
diff --git a/src/misc/fetch-meta.ts b/src/misc/fetch-meta.ts
index 680cf37a72..e7a945dc9e 100644
--- a/src/misc/fetch-meta.ts
+++ b/src/misc/fetch-meta.ts
@@ -32,4 +32,4 @@ setInterval(() => {
fetchMeta(true).then(meta => {
cache = meta;
});
-}, 5000);
+}, 1000 * 10);
diff --git a/src/misc/keypair-store.ts b/src/misc/keypair-store.ts
new file mode 100644
index 0000000000..c78fdd7555
--- /dev/null
+++ b/src/misc/keypair-store.ts
@@ -0,0 +1,10 @@
+import { UserKeypairs } from '../models';
+import { User } from '../models/entities/user';
+import { UserKeypair } from '../models/entities/user-keypair';
+import { Cache } from './cache';
+
+const cache = new Cache<UserKeypair>(Infinity);
+
+export async function getUserKeypair(userId: User['id']): Promise<UserKeypair> {
+ return await cache.fetch(userId, () => UserKeypairs.findOneOrFail(userId));
+}
diff --git a/src/misc/populate-emojis.ts b/src/misc/populate-emojis.ts
new file mode 100644
index 0000000000..8052c71489
--- /dev/null
+++ b/src/misc/populate-emojis.ts
@@ -0,0 +1,119 @@
+import { In } from 'typeorm';
+import { Emojis } from '../models';
+import { Emoji } from '../models/entities/emoji';
+import { Note } from '../models/entities/note';
+import { Cache } from './cache';
+import { isSelfHost, toPunyNullable } from './convert-host';
+import { decodeReaction } from './reaction-lib';
+
+const cache = new Cache<Emoji | null>(1000 * 60 * 60 * 12);
+
+/**
+ * 添付用絵文字情報
+ */
+type PopulatedEmoji = {
+ name: string;
+ url: string;
+};
+
+function normalizeHost(src: string | undefined, noteUserHost: string | null): string | null {
+ // クエリに使うホスト
+ let host = src === '.' ? null // .はローカルホスト (ここがマッチするのはリアクションのみ)
+ : src === undefined ? noteUserHost // ノートなどでホスト省略表記の場合はローカルホスト (ここがリアクションにマッチすることはない)
+ : isSelfHost(src) ? null // 自ホスト指定
+ : (src || noteUserHost); // 指定されたホスト || ノートなどの所有者のホスト (こっちがリアクションにマッチすることはない)
+
+ host = toPunyNullable(host);
+
+ return host;
+}
+
+function parseEmojiStr(emojiName: string, noteUserHost: string | null) {
+ const match = emojiName.match(/^(\w+)(?:@([\w.-]+))?$/);
+ if (!match) return { name: null, host: null };
+
+ const name = match[1];
+
+ // ホスト正規化
+ const host = toPunyNullable(normalizeHost(match[2], noteUserHost));
+
+ return { name, host };
+}
+
+/**
+ * 添付用絵文字情報を解決する
+ * @param emojiName ノートやユーザープロフィールに添付された、またはリアクションのカスタム絵文字名 (:は含めない, リアクションでローカルホストの場合は@.を付ける (これはdecodeReactionで可能))
+ * @param noteUserHost ノートやユーザープロフィールの所有者のホスト
+ * @returns 絵文字情報, nullは未マッチを意味する
+ */
+export async function populateEmoji(emojiName: string, noteUserHost: string | null): Promise<PopulatedEmoji | null> {
+ const { name, host } = parseEmojiStr(emojiName, noteUserHost);
+ if (name == null) return null;
+
+ const queryOrNull = async () => (await Emojis.findOne({
+ name,
+ host
+ })) || null;
+
+ const emoji = await cache.fetch(`${name} ${host}`, queryOrNull);
+
+ if (emoji == null) return null;
+
+ return {
+ name: emojiName,
+ url: emoji.url,
+ };
+}
+
+/**
+ * 複数の添付用絵文字情報を解決する (キャシュ付き, 存在しないものは結果から除外される)
+ */
+export async function populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise<PopulatedEmoji[]> {
+ const emojis = await Promise.all(emojiNames.map(x => populateEmoji(x, noteUserHost)));
+ return emojis.filter((x): x is PopulatedEmoji => x != null);
+}
+
+export function aggregateNoteEmojis(notes: Note[]) {
+ let emojis: { name: string | null; host: string | null; }[] = [];
+ for (const note of notes) {
+ emojis = emojis.concat(note.emojis
+ .map(e => parseEmojiStr(e, note.userHost)));
+ if (note.renote) {
+ emojis = emojis.concat(note.renote.emojis
+ .map(e => parseEmojiStr(e, note.renote!.userHost)));
+ if (note.renote.user) {
+ emojis = emojis.concat(note.renote.user.emojis
+ .map(e => parseEmojiStr(e, note.renote!.userHost)));
+ }
+ }
+ const customReactions = Object.keys(note.reactions).map(x => decodeReaction(x)).filter(x => x.name != null) as typeof emojis;
+ emojis = emojis.concat(customReactions);
+ if (note.user) {
+ emojis = emojis.concat(note.user.emojis
+ .map(e => parseEmojiStr(e, note.userHost)));
+ }
+ }
+ return emojis.filter(x => x.name != null) as { name: string; host: string | null; }[];
+}
+
+/**
+ * 与えられた絵文字のリストをデータベースから取得し、キャッシュに追加します
+ */
+export async function prefetchEmojis(emojis: { name: string; host: string | null; }[]): Promise<void> {
+ const notCachedEmojis = emojis.filter(emoji => cache.get(`${emoji.name} ${emoji.host}`) == null);
+ const emojisQuery: any[] = [];
+ const hosts = new Set(notCachedEmojis.map(e => e.host));
+ for (const host of hosts) {
+ emojisQuery.push({
+ name: In(notCachedEmojis.filter(e => e.host === host).map(e => e.name)),
+ host: host
+ });
+ }
+ const _emojis = emojisQuery.length > 0 ? await Emojis.find({
+ where: emojisQuery,
+ select: ['name', 'host', 'url']
+ }) : [];
+ for (const emoji of _emojis) {
+ cache.set(`${emoji.name} ${emoji.host}`, emoji);
+ }
+}
diff --git a/src/models/entities/drive-file.ts b/src/models/entities/drive-file.ts
index 680a40bc06..698dfac222 100644
--- a/src/models/entities/drive-file.ts
+++ b/src/models/entities/drive-file.ts
@@ -77,7 +77,7 @@ export class DriveFile {
default: {},
comment: 'The any properties of the DriveFile. For example, it includes image width/height.'
})
- public properties: Record<string, any>;
+ public properties: { width?: number; height?: number; avgColor?: string };
@Index()
@Column('boolean')
diff --git a/src/models/entities/note-reaction.ts b/src/models/entities/note-reaction.ts
index 69bb663fd3..674dc3639e 100644
--- a/src/models/entities/note-reaction.ts
+++ b/src/models/entities/note-reaction.ts
@@ -23,7 +23,7 @@ export class NoteReaction {
onDelete: 'CASCADE'
})
@JoinColumn()
- public user: User | null;
+ public user?: User | null;
@Index()
@Column(id())
@@ -33,7 +33,7 @@ export class NoteReaction {
onDelete: 'CASCADE'
})
@JoinColumn()
- public note: Note | null;
+ public note?: Note | null;
// TODO: 対象noteのuserIdを非正規化したい(「受け取ったリアクション一覧」のようなものを(JOIN無しで)実装したいため)
diff --git a/src/models/repositories/antenna.ts b/src/models/repositories/antenna.ts
index 0f0a5c0171..84e082bd25 100644
--- a/src/models/repositories/antenna.ts
+++ b/src/models/repositories/antenna.ts
@@ -56,16 +56,24 @@ export const packedAntennaSchema = {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
- type: 'string' as const,
- optional: false as const, nullable: false as const
+ type: 'array' as const,
+ optional: false as const, nullable: false as const,
+ items: {
+ type: 'string' as const,
+ optional: false as const, nullable: false as const
+ }
}
},
- execludeKeywords: {
+ excludeKeywords: {
type: 'array' as const,
optional: false as const, nullable: false as const,
items: {
- type: 'string' as const,
- optional: false as const, nullable: false as const
+ type: 'array' as const,
+ optional: false as const, nullable: false as const,
+ items: {
+ type: 'string' as const,
+ optional: false as const, nullable: false as const
+ }
}
},
src: {
diff --git a/src/models/repositories/drive-file.ts b/src/models/repositories/drive-file.ts
index 5085c76e92..61d24bd24e 100644
--- a/src/models/repositories/drive-file.ts
+++ b/src/models/repositories/drive-file.ts
@@ -12,6 +12,12 @@ import { fetchMeta } from '../../misc/fetch-meta';
export type PackedDriveFile = SchemaType<typeof packedDriveFileSchema>;
+type PackOptions = {
+ detail?: boolean,
+ self?: boolean,
+ withUser?: boolean,
+};
+
@EntityRepository(DriveFile)
export class DriveFileRepository extends Repository<DriveFile> {
public validateFileName(name: string): boolean {
@@ -89,20 +95,19 @@ export class DriveFileRepository extends Repository<DriveFile> {
return parseInt(sum, 10) || 0;
}
+ public async pack(src: DriveFile['id'], options?: PackOptions): Promise<PackedDriveFile | null>;
+ public async pack(src: DriveFile, options?: PackOptions): Promise<PackedDriveFile>;
public async pack(
src: DriveFile['id'] | DriveFile,
- options?: {
- detail?: boolean,
- self?: boolean,
- withUser?: boolean,
- }
- ): Promise<PackedDriveFile> {
+ options?: PackOptions
+ ): Promise<PackedDriveFile | null> {
const opts = Object.assign({
detail: false,
self: false
}, options);
- const file = typeof src === 'object' ? src : await this.findOneOrFail(src);
+ const file = typeof src === 'object' ? src : await this.findOne(src);
+ if (file == null) return null;
const meta = await fetchMeta();
@@ -128,15 +133,12 @@ export class DriveFileRepository extends Repository<DriveFile> {
});
}
- public packMany(
- files: any[],
- options?: {
- detail?: boolean
- self?: boolean,
- withUser?: boolean,
- }
+ public async packMany(
+ files: (DriveFile['id'] | DriveFile)[],
+ options?: PackOptions
) {
- return Promise.all(files.map(f => this.pack(f, options)));
+ const items = await Promise.all(files.map(f => this.pack(f, options)));
+ return items.filter(x => x != null);
}
}
@@ -197,12 +199,12 @@ export const packedDriveFileSchema = {
properties: {
width: {
type: 'number' as const,
- optional: false as const, nullable: false as const,
+ optional: true as const, nullable: false as const,
example: 1280
},
height: {
type: 'number' as const,
- optional: false as const, nullable: false as const,
+ optional: true as const, nullable: false as const,
example: 720
},
avgColor: {
diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts
index 32552db2fe..73e18f6c5b 100644
--- a/src/models/repositories/note.ts
+++ b/src/models/repositories/note.ts
@@ -1,14 +1,14 @@
import { EntityRepository, Repository, In } from 'typeorm';
import { Note } from '../entities/note';
import { User } from '../entities/user';
-import { Emojis, Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls, Channels } from '..';
+import { Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls, Channels } from '..';
import { SchemaType } from '../../misc/schema';
import { awaitAll } from '../../prelude/await-all';
import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '../../misc/reaction-lib';
import { toString } from '../../mfm/to-string';
import { parse } from '../../mfm/parse';
-import { Emoji } from '../entities/emoji';
-import { concat } from '../../prelude/array';
+import { NoteReaction } from '../entities/note-reaction';
+import { aggregateNoteEmojis, populateEmojis, prefetchEmojis } from '../../misc/populate-emojis';
export type PackedNote = SchemaType<typeof packedNoteSchema>;
@@ -83,6 +83,9 @@ export class NoteRepository extends Repository<Note> {
options?: {
detail?: boolean;
skipHide?: boolean;
+ _hint_?: {
+ myReactions: Map<Note['id'], NoteReaction | null>;
+ };
}
): Promise<PackedNote> {
const opts = Object.assign({
@@ -130,64 +133,17 @@ export class NoteRepository extends Repository<Note> {
};
}
- /**
- * 添付用emojisを解決する
- * @param emojiNames Note等に添付されたカスタム絵文字名 (:は含めない)
- * @param noteUserHost Noteのホスト
- * @param reactionNames Note等にリアクションされたカスタム絵文字名 (:は含めない)
- */
- async function populateEmojis(emojiNames: string[], noteUserHost: string | null, reactionNames: string[]) {
- let all = [] as {
- name: string,
- url: string
- }[];
-
- // カスタム絵文字
- if (emojiNames?.length > 0) {
- const tmp = await Emojis.find({
- where: {
- name: In(emojiNames),
- host: noteUserHost
- },
- select: ['name', 'host', 'url']
- }).then(emojis => emojis.map((emoji: Emoji) => {
- return {
- name: emoji.name,
- url: emoji.url,
- };
- }));
-
- all = concat([all, tmp]);
- }
-
- const customReactions = reactionNames?.map(x => decodeReaction(x)).filter(x => x.name);
-
- if (customReactions?.length > 0) {
- const where = [] as {}[];
-
- for (const customReaction of customReactions) {
- where.push({
- name: customReaction.name,
- host: customReaction.host
- });
+ async function populateMyReaction() {
+ if (options?._hint_?.myReactions) {
+ const reaction = options._hint_.myReactions.get(note.id);
+ if (reaction) {
+ return convertLegacyReaction(reaction.reaction);
+ } else if (reaction === null) {
+ return undefined;
}
-
- const tmp = await Emojis.find({
- where,
- select: ['name', 'host', 'url']
- }).then(emojis => emojis.map((emoji: Emoji) => {
- return {
- name: `${emoji.name}@${emoji.host || '.'}`, // @host付きでローカルは.
- url: emoji.url,
- };
- }));
- all = concat([all, tmp]);
+ // 実装上抜けがあるだけかもしれないので、「ヒントに含まれてなかったら(=undefinedなら)return」のようにはしない
}
- return all;
- }
-
- async function populateMyReaction() {
const reaction = await NoteReactions.findOne({
userId: meId!,
noteId: note.id,
@@ -212,11 +168,15 @@ export class NoteRepository extends Repository<Note> {
: await Channels.findOne(note.channelId)
: null;
+ const reactionEmojiNames = Object.keys(note.reactions).filter(x => x?.startsWith(':')).map(x => decodeReaction(x).reaction).map(x => x.replace(/:/g, ''));
+
const packed = await awaitAll({
id: note.id,
createdAt: note.createdAt.toISOString(),
userId: note.userId,
- user: Users.pack(note.user || note.userId, meId),
+ user: Users.pack(note.user || note.userId, meId, {
+ detail: false,
+ }),
text: text,
cw: note.cw,
visibility: note.visibility,
@@ -227,7 +187,7 @@ export class NoteRepository extends Repository<Note> {
repliesCount: note.repliesCount,
reactions: convertLegacyReactions(note.reactions),
tags: note.tags.length > 0 ? note.tags : undefined,
- emojis: populateEmojis(note.emojis, host, Object.keys(note.reactions)),
+ emojis: populateEmojis(note.emojis.concat(reactionEmojiNames), host),
fileIds: note.fileIds,
files: DriveFiles.packMany(note.fileIds),
replyId: note.replyId,
@@ -244,12 +204,14 @@ export class NoteRepository extends Repository<Note> {
_prId_: (note as any)._prId_ || undefined,
...(opts.detail ? {
- reply: note.replyId ? this.pack(note.replyId, meId, {
- detail: false
+ reply: note.replyId ? this.pack(note.reply || note.replyId, meId, {
+ detail: false,
+ _hint_: options?._hint_
}) : undefined,
- renote: note.renoteId ? this.pack(note.renoteId, meId, {
- detail: true
+ renote: note.renoteId ? this.pack(note.renote || note.renoteId, meId, {
+ detail: true,
+ _hint_: options?._hint_
}) : undefined,
poll: note.hasPoll ? populatePoll() : undefined,
@@ -272,15 +234,39 @@ export class NoteRepository extends Repository<Note> {
return packed;
}
- public packMany(
- notes: (Note['id'] | Note)[],
+ public async packMany(
+ notes: Note[],
me?: User['id'] | User | null | undefined,
options?: {
detail?: boolean;
skipHide?: boolean;
}
) {
- return Promise.all(notes.map(n => this.pack(n, me, options)));
+ if (notes.length === 0) return [];
+
+ const meId = me ? typeof me === 'string' ? me : me.id : null;
+ const myReactionsMap = new Map<Note['id'], NoteReaction | null>();
+ if (meId) {
+ const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!);
+ const targets = [...notes.map(n => n.id), ...renoteIds];
+ const myReactions = await NoteReactions.find({
+ userId: meId,
+ noteId: In(targets),
+ });
+
+ for (const target of targets) {
+ myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) || null);
+ }
+ }
+
+ await prefetchEmojis(aggregateNoteEmojis(notes));
+
+ return await Promise.all(notes.map(n => this.pack(n, me, {
+ ...options,
+ _hint_: {
+ myReactions: myReactionsMap
+ }
+ })));
}
}
diff --git a/src/models/repositories/notification.ts b/src/models/repositories/notification.ts
index 16de6c8c25..83fe11d5f7 100644
--- a/src/models/repositories/notification.ts
+++ b/src/models/repositories/notification.ts
@@ -1,8 +1,12 @@
-import { EntityRepository, Repository } from 'typeorm';
-import { Users, Notes, UserGroupInvitations, AccessTokens } from '..';
+import { EntityRepository, In, Repository } from 'typeorm';
+import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '..';
import { Notification } from '../entities/notification';
import { awaitAll } from '../../prelude/await-all';
import { SchemaType } from '../../misc/schema';
+import { Note } from '../entities/note';
+import { NoteReaction } from '../entities/note-reaction';
+import { User } from '../entities/user';
+import { aggregateNoteEmojis, prefetchEmojis } from '../../misc/populate-emojis';
export type PackedNotification = SchemaType<typeof packedNotificationSchema>;
@@ -10,6 +14,11 @@ export type PackedNotification = SchemaType<typeof packedNotificationSchema>;
export class NotificationRepository extends Repository<Notification> {
public async pack(
src: Notification['id'] | Notification,
+ options: {
+ _hintForEachNotes_?: {
+ myReactions: Map<Note['id'], NoteReaction | null>;
+ };
+ }
): Promise<PackedNotification> {
const notification = typeof src === 'object' ? src : await this.findOneOrFail(src);
const token = notification.appAccessTokenId ? await AccessTokens.findOneOrFail(notification.appAccessTokenId) : null;
@@ -22,23 +31,41 @@ export class NotificationRepository extends Repository<Notification> {
userId: notification.notifierId,
user: notification.notifierId ? Users.pack(notification.notifier || notification.notifierId) : null,
...(notification.type === 'mention' ? {
- note: Notes.pack(notification.note || notification.noteId!, notification.notifieeId),
+ note: Notes.pack(notification.note || notification.noteId!, notification.notifieeId, {
+ detail: true,
+ _hint_: options._hintForEachNotes_
+ }),
} : {}),
...(notification.type === 'reply' ? {
- note: Notes.pack(notification.note || notification.noteId!, notification.notifieeId),
+ note: Notes.pack(notification.note || notification.noteId!, notification.notifieeId, {
+ detail: true,
+ _hint_: options._hintForEachNotes_
+ }),
} : {}),
...(notification.type === 'renote' ? {
- note: Notes.pack(notification.note || notification.noteId!, notification.notifieeId),
+ note: Notes.pack(notification.note || notification.noteId!, notification.notifieeId, {
+ detail: true,
+ _hint_: options._hintForEachNotes_
+ }),
} : {}),
...(notification.type === 'quote' ? {
- note: Notes.pack(notification.note || notification.noteId!, notification.notifieeId),
+ note: Notes.pack(notification.note || notification.noteId!, notification.notifieeId, {
+ detail: true,
+ _hint_: options._hintForEachNotes_
+ }),
} : {}),
...(notification.type === 'reaction' ? {
- note: Notes.pack(notification.note || notification.noteId!, notification.notifieeId),
+ note: Notes.pack(notification.note || notification.noteId!, notification.notifieeId, {
+ detail: true,
+ _hint_: options._hintForEachNotes_
+ }),
reaction: notification.reaction
} : {}),
...(notification.type === 'pollVote' ? {
- note: Notes.pack(notification.note || notification.noteId!, notification.notifieeId),
+ note: Notes.pack(notification.note || notification.noteId!, notification.notifieeId, {
+ detail: true,
+ _hint_: options._hintForEachNotes_
+ }),
choice: notification.choice
} : {}),
...(notification.type === 'groupInvited' ? {
@@ -52,10 +79,33 @@ export class NotificationRepository extends Repository<Notification> {
});
}
- public packMany(
- notifications: any[],
+ public async packMany(
+ notifications: Notification[],
+ meId: User['id']
) {
- return Promise.all(notifications.map(x => this.pack(x)));
+ if (notifications.length === 0) return [];
+
+ const notes = notifications.filter(x => x.note != null).map(x => x.note!);
+ const noteIds = notes.map(n => n.id);
+ const myReactionsMap = new Map<Note['id'], NoteReaction | null>();
+ const renoteIds = notes.filter(n => n.renoteId != null).map(n => n.renoteId!);
+ const targets = [...noteIds, ...renoteIds];
+ const myReactions = await NoteReactions.find({
+ userId: meId,
+ noteId: In(targets),
+ });
+
+ for (const target of targets) {
+ myReactionsMap.set(target, myReactions.find(reaction => reaction.noteId === target) || null);
+ }
+
+ await prefetchEmojis(aggregateNoteEmojis(notes));
+
+ return await Promise.all(notifications.map(x => this.pack(x, {
+ _hintForEachNotes_: {
+ myReactions: myReactionsMap
+ }
+ })));
}
}
diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts
index 3a6ab48c5f..53c06f3f16 100644
--- a/src/models/repositories/user.ts
+++ b/src/models/repositories/user.ts
@@ -1,10 +1,11 @@
import $ from 'cafy';
import { EntityRepository, Repository, In, Not } from 'typeorm';
import { User, ILocalUser, IRemoteUser } from '../entities/user';
-import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances } from '..';
+import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances } from '..';
import config from '../../config';
import { SchemaType } from '../../misc/schema';
import { awaitAll } from '../../prelude/await-all';
+import { populateEmojis } from '../../misc/populate-emojis';
export type PackedUser = SchemaType<typeof packedUserSchema>;
@@ -160,10 +161,11 @@ export class UserRepository extends Repository<User> {
const meId = me ? typeof me === 'string' ? me : me.id : null;
const relation = meId && (meId !== user.id) && opts.detail ? await this.getRelation(meId, user.id) : null;
- const pins = opts.detail ? await UserNotePinings.find({
- where: { userId: user.id },
- order: { id: 'DESC' }
- }) : [];
+ const pins = opts.detail ? await UserNotePinings.createQueryBuilder('pin')
+ .where('pin.userId = :userId', { userId: user.id })
+ .innerJoinAndSelect('pin.note', 'note')
+ .orderBy('pin.id', 'DESC')
+ .getMany() : [];
const profile = opts.detail ? await UserProfiles.findOneOrFail(user.id) : null;
const falsy = opts.detail ? false : undefined;
@@ -188,15 +190,7 @@ export class UserRepository extends Repository<User> {
faviconUrl: instance.faviconUrl,
themeColor: instance.themeColor,
} : undefined) : undefined,
-
- // カスタム絵文字添付
- emojis: user.emojis.length > 0 ? Emojis.find({
- where: {
- name: In(user.emojis),
- host: user.host
- },
- select: ['name', 'host', 'url', 'aliases']
- }) : [],
+ emojis: populateEmojis(user.emojis, user.host),
...(opts.detail ? {
url: profile!.url,
@@ -218,7 +212,7 @@ export class UserRepository extends Repository<User> {
followingCount: user.followingCount,
notesCount: user.notesCount,
pinnedNoteIds: pins.map(pin => pin.noteId),
- pinnedNotes: Notes.packMany(pins.map(pin => pin.noteId), meId, {
+ pinnedNotes: Notes.packMany(pins.map(pin => pin.note!), meId, {
detail: true
}),
pinnedPageId: profile!.pinnedPageId,
diff --git a/src/queue/index.ts b/src/queue/index.ts
index 163c57d691..9fb4595a35 100644
--- a/src/queue/index.ts
+++ b/src/queue/index.ts
@@ -1,4 +1,3 @@
-import * as Queue from 'bull';
import * as httpSignature from 'http-signature';
import config from '../config';
@@ -13,22 +12,7 @@ import { queueLogger } from './logger';
import { DriveFile } from '../models/entities/drive-file';
import { getJobInfo } from './get-job-info';
import { IActivity } from '../remote/activitypub/type';
-
-function initializeQueue(name: string, limitPerSec = -1) {
- return new Queue(name, {
- redis: {
- port: config.redis.port,
- host: config.redis.host,
- password: config.redis.pass,
- db: config.redis.db || 0,
- },
- prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue',
- limiter: limitPerSec > 0 ? {
- max: limitPerSec * 5,
- duration: 5000
- } : undefined
- });
-}
+import { dbQueue, deliverQueue, inboxQueue, objectStorageQueue } from './queues';
export type InboxJobData = {
activity: IActivity,
@@ -44,11 +28,6 @@ function renderError(e: Error): any {
};
}
-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');
-
const deliverLogger = queueLogger.createSubLogger('deliver');
const inboxLogger = queueLogger.createSubLogger('inbox');
const dbLogger = queueLogger.createSubLogger('db');
diff --git a/src/queue/initialize.ts b/src/queue/initialize.ts
new file mode 100644
index 0000000000..92579531e4
--- /dev/null
+++ b/src/queue/initialize.ts
@@ -0,0 +1,18 @@
+import * as Queue from 'bull';
+import config from '../config';
+
+export function initialize(name: string, limitPerSec = -1) {
+ return new Queue(name, {
+ redis: {
+ port: config.redis.port,
+ host: config.redis.host,
+ password: config.redis.pass,
+ db: config.redis.db || 0,
+ },
+ prefix: config.redis.prefix ? `${config.redis.prefix}:queue` : 'queue',
+ limiter: limitPerSec > 0 ? {
+ max: limitPerSec * 5,
+ duration: 5000
+ } : undefined
+ });
+}
diff --git a/src/queue/processors/deliver.ts b/src/queue/processors/deliver.ts
index cb7587ef81..a8b4ed4fe3 100644
--- a/src/queue/processors/deliver.ts
+++ b/src/queue/processors/deliver.ts
@@ -7,11 +7,15 @@ import { instanceChart } from '../../services/chart';
import { fetchInstanceMetadata } from '../../services/fetch-instance-metadata';
import { fetchMeta } from '../../misc/fetch-meta';
import { toPuny } from '../../misc/convert-host';
+import { Cache } from '../../misc/cache';
+import { Instance } from '../../models/entities/instance';
const logger = new Logger('deliver');
let latest: string | null = null;
+const suspendedHostsCache = new Cache<Instance[]>(1000 * 60 * 60);
+
export default async (job: Bull.Job) => {
const { host } = new URL(job.data.to);
@@ -22,12 +26,15 @@ export default async (job: Bull.Job) => {
}
// isSuspendedなら中断
- const suspendedHosts = await Instances.find({
- where: {
- isSuspended: true
- },
- cache: 60 * 1000
- });
+ let suspendedHosts = suspendedHostsCache.get(null);
+ if (suspendedHosts == null) {
+ suspendedHosts = await Instances.find({
+ where: {
+ isSuspended: true
+ },
+ });
+ suspendedHostsCache.set(null, suspendedHosts);
+ }
if (suspendedHosts.map(x => x.host).includes(toPuny(host))) {
return 'skip (suspended)';
}
diff --git a/src/queue/processors/inbox.ts b/src/queue/processors/inbox.ts
index b4e8b85a46..a5822ff25f 100644
--- a/src/queue/processors/inbox.ts
+++ b/src/queue/processors/inbox.ts
@@ -40,6 +40,7 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
return `Old keyId is no longer supported. ${keyIdLower}`;
}
+ // TDOO: キャッシュ
const dbResolver = new DbResolver();
// HTTP-Signature keyIdを元にDBから取得
diff --git a/src/queue/queues.ts b/src/queue/queues.ts
new file mode 100644
index 0000000000..d589d9f7da
--- /dev/null
+++ b/src/queue/queues.ts
@@ -0,0 +1,7 @@
+import config from '../config';
+import { initialize as initializeQueue } from './initialize';
+
+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');
diff --git a/src/remote/activitypub/deliver-manager.ts b/src/remote/activitypub/deliver-manager.ts
index d147b3c9b0..92721f5525 100644
--- a/src/remote/activitypub/deliver-manager.ts
+++ b/src/remote/activitypub/deliver-manager.ts
@@ -76,7 +76,7 @@ export default class DeliverManager {
public async execute() {
if (!Users.isLocalUser(this.actor)) return;
- const inboxes: string[] = [];
+ const inboxes = new Set<string>();
// build inbox list
for (const recipe of this.recipes) {
@@ -89,13 +89,13 @@ export default class DeliverManager {
for (const following of followers) {
if (Followings.isRemoteFollower(following)) {
const inbox = following.followerSharedInbox || following.followerInbox;
- if (!inboxes.includes(inbox)) inboxes.push(inbox);
+ inboxes.add(inbox);
}
}
} else if (isDirect(recipe)) {
// direct deliver
const inbox = recipe.to.inbox;
- if (inbox && !inboxes.includes(inbox)) inboxes.push(inbox);
+ if (inbox) inboxes.add(inbox);
}
}
diff --git a/src/remote/activitypub/renderer/index.ts b/src/remote/activitypub/renderer/index.ts
index e74affdadf..4c33fdafb1 100644
--- a/src/remote/activitypub/renderer/index.ts
+++ b/src/remote/activitypub/renderer/index.ts
@@ -3,7 +3,7 @@ import { v4 as uuid } from 'uuid';
import { IActivity } from '../type';
import { LdSignature } from '../misc/ld-signature';
import { ILocalUser } from '../../../models/entities/user';
-import { UserKeypairs } from '../../../models';
+import { getUserKeypair } from '../../../misc/keypair-store';
export const renderActivity = (x: any): IActivity | null => {
if (x == null) return null;
@@ -23,9 +23,7 @@ export const renderActivity = (x: any): IActivity | null => {
export const attachLdSignature = async (activity: any, user: ILocalUser): Promise<IActivity | null> => {
if (activity == null) return null;
- const keypair = await UserKeypairs.findOneOrFail({
- userId: user.id
- });
+ const keypair = await getUserKeypair(user.id);
const obj = {
// as non-standards
diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts
index 4907e3bc6f..479e6d76bf 100644
--- a/src/remote/activitypub/renderer/person.ts
+++ b/src/remote/activitypub/renderer/person.ts
@@ -8,7 +8,8 @@ import { getEmojis } from './note';
import renderEmoji from './emoji';
import { IIdentifier } from '../models/identifier';
import renderHashtag from './hashtag';
-import { DriveFiles, UserProfiles, UserKeypairs } from '../../../models';
+import { DriveFiles, UserProfiles } from '../../../models';
+import { getUserKeypair } from '../../../misc/keypair-store';
export async function renderPerson(user: ILocalUser) {
const id = `${config.url}/users/${user.id}`;
@@ -49,7 +50,7 @@ export async function renderPerson(user: ILocalUser) {
...hashtagTags,
];
- const keypair = await UserKeypairs.findOneOrFail(user.id);
+ const keypair = await getUserKeypair(user.id);
const person = {
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
diff --git a/src/remote/activitypub/request.ts b/src/remote/activitypub/request.ts
index 2f07351635..5f15d5480c 100644
--- a/src/remote/activitypub/request.ts
+++ b/src/remote/activitypub/request.ts
@@ -5,11 +5,11 @@ import * as crypto from 'crypto';
import config from '../../config';
import { ILocalUser } from '../../models/entities/user';
-import { UserKeypairs } from '../../models';
import { getAgentByUrl } from '../../misc/fetch';
import { URL } from 'url';
import got from 'got';
import * as Got from 'got';
+import { getUserKeypair } from '../../misc/keypair-store';
export default async (user: ILocalUser, url: string, object: any) => {
const timeout = 10 * 1000;
@@ -22,9 +22,7 @@ export default async (user: ILocalUser, url: string, object: any) => {
sha256.update(data);
const hash = sha256.digest('base64');
- const keypair = await UserKeypairs.findOneOrFail({
- userId: user.id
- });
+ const keypair = await getUserKeypair(user.id);
await new Promise((resolve, reject) => {
const req = https.request({
@@ -74,9 +72,7 @@ export default async (user: ILocalUser, url: string, object: any) => {
export async function signedGet(url: string, user: ILocalUser) {
const timeout = 10 * 1000;
- const keypair = await UserKeypairs.findOneOrFail({
- userId: user.id
- });
+ const keypair = await getUserKeypair(user.id);
const req = got.get<any>(url, {
headers: {
diff --git a/src/server/activitypub.ts b/src/server/activitypub.ts
index bf71258625..694807239b 100644
--- a/src/server/activitypub.ts
+++ b/src/server/activitypub.ts
@@ -13,10 +13,11 @@ import Following from './activitypub/following';
import Featured from './activitypub/featured';
import { inbox as processInbox } from '../queue';
import { isSelfHost } from '../misc/convert-host';
-import { Notes, Users, Emojis, UserKeypairs, NoteReactions } from '../models';
+import { Notes, Users, Emojis, NoteReactions } from '../models';
import { ILocalUser, User } from '../models/entities/user';
import { In } from 'typeorm';
import { renderLike } from '../remote/activitypub/renderer/like';
+import { getUserKeypair } from '../misc/keypair-store';
// Init router
const router = new Router();
@@ -135,7 +136,7 @@ router.get('/users/:user/publickey', async ctx => {
return;
}
- const keypair = await UserKeypairs.findOneOrFail(user.id);
+ const keypair = await getUserKeypair(user.id);
if (Users.isLocalUser(user)) {
ctx.body = renderActivity(renderKey(user, keypair));
diff --git a/src/server/api/authenticate.ts b/src/server/api/authenticate.ts
index 0374ca35ea..9c9ef74352 100644
--- a/src/server/api/authenticate.ts
+++ b/src/server/api/authenticate.ts
@@ -2,6 +2,11 @@ import isNativeToken from './common/is-native-token';
import { User } from '../../models/entities/user';
import { Users, AccessTokens, Apps } from '../../models';
import { AccessToken } from '../../models/entities/access-token';
+import { Cache } from '../../misc/cache';
+
+// TODO: TypeORMのカスタムキャッシュプロバイダを使っても良いかも
+// ref. https://github.com/typeorm/typeorm/blob/master/docs/caching.md
+const cache = new Cache<User>(1000 * 60 * 60);
export default async (token: string): Promise<[User | null | undefined, AccessToken | null | undefined]> => {
if (token == null) {
@@ -9,6 +14,11 @@ export default async (token: string): Promise<[User | null | undefined, AccessTo
}
if (isNativeToken(token)) {
+ const cached = cache.get(token);
+ if (cached) {
+ return [cached, null];
+ }
+
// Fetch user
const user = await Users
.findOne({ token });
@@ -17,8 +27,11 @@ export default async (token: string): Promise<[User | null | undefined, AccessTo
throw new Error('user not found');
}
+ cache.set(token, user);
+
return [user, null];
} else {
+ // TODO: cache
const accessToken = await AccessTokens.findOne({
where: [{
hash: token.toLowerCase() // app
diff --git a/src/server/api/common/inject-featured.ts b/src/server/api/common/inject-featured.ts
index 3f47c13385..bbed7f69cb 100644
--- a/src/server/api/common/inject-featured.ts
+++ b/src/server/api/common/inject-featured.ts
@@ -23,7 +23,7 @@ export async function injectFeatured(timeline: Note[], user?: User | null) {
.andWhere(`note.score > 0`)
.andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) })
.andWhere(`note.visibility = 'public'`)
- .leftJoinAndSelect('note.user', 'user');
+ .innerJoinAndSelect('note.user', 'user');
if (user) {
query.andWhere('note.userId != :userId', { userId: user.id });
diff --git a/src/server/api/define.ts b/src/server/api/define.ts
index 1c7ee26479..4e59357c13 100644
--- a/src/server/api/define.ts
+++ b/src/server/api/define.ts
@@ -18,6 +18,7 @@ type executor<T extends IEndpointMeta> =
(params: Params<T>, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken | null, file?: any, cleanup?: Function) =>
Promise<T['res'] extends undefined ? Response : SchemaType<NonNullable<T['res']>>>;
+// TODO: API関数に user まるごと渡すのではなくユーザーIDなどの最小限のプロパティだけ渡すようにしたい(キャッシュとか考えないでよくなるため)
export default function <T extends IEndpointMeta>(meta: T, cb: executor<T>)
: (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken | null, file?: any) => Promise<any> {
return (params: any, user: T['requireCredential'] extends true ? ILocalUser : ILocalUser | null, token: AccessToken | null, file?: any) => {
diff --git a/src/server/api/endpoints/admin/invite.ts b/src/server/api/endpoints/admin/invite.ts
index 4529d16adf..987105791f 100644
--- a/src/server/api/endpoints/admin/invite.ts
+++ b/src/server/api/endpoints/admin/invite.ts
@@ -38,7 +38,7 @@ export default define(meta, async () => {
chars: '2-9A-HJ-NP-Z', // [0-9A-Z] w/o [01IO] (32 patterns)
});
- await RegistrationTickets.save({
+ await RegistrationTickets.insert({
id: genId(),
createdAt: new Date(),
code,
diff --git a/src/server/api/endpoints/admin/promo/create.ts b/src/server/api/endpoints/admin/promo/create.ts
index 8b96d563c2..aa22e68ebd 100644
--- a/src/server/api/endpoints/admin/promo/create.ts
+++ b/src/server/api/endpoints/admin/promo/create.ts
@@ -53,7 +53,7 @@ export default define(meta, async (ps, user) => {
throw new ApiError(meta.errors.alreadyPromoted);
}
- await PromoNotes.save({
+ await PromoNotes.insert({
noteId: note.id,
createdAt: new Date(),
expiresAt: new Date(ps.expiresAt),
diff --git a/src/server/api/endpoints/antennas/notes.ts b/src/server/api/endpoints/antennas/notes.ts
index 750fc080cf..6fd3cf2df5 100644
--- a/src/server/api/endpoints/antennas/notes.ts
+++ b/src/server/api/endpoints/antennas/notes.ts
@@ -73,7 +73,11 @@ export default define(meta, async (ps, user) => {
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere(`note.id IN (${ antennaQuery.getQuery() })`)
- .leftJoinAndSelect('note.user', 'user')
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
.setParameters(antennaQuery.getParameters());
generateVisibilityQuery(query, user);
diff --git a/src/server/api/endpoints/auth/accept.ts b/src/server/api/endpoints/auth/accept.ts
index 6d4d31fa1e..444053a946 100644
--- a/src/server/api/endpoints/auth/accept.ts
+++ b/src/server/api/endpoints/auth/accept.ts
@@ -58,7 +58,7 @@ export default define(meta, async (ps, user) => {
const now = new Date();
// Insert access token doc
- await AccessTokens.save({
+ await AccessTokens.insert({
id: genId(),
createdAt: now,
lastUsedAt: now,
diff --git a/src/server/api/endpoints/channels/follow.ts b/src/server/api/endpoints/channels/follow.ts
index bf2f2bbb57..c5976a8a34 100644
--- a/src/server/api/endpoints/channels/follow.ts
+++ b/src/server/api/endpoints/channels/follow.ts
@@ -4,6 +4,7 @@ import define from '../../define';
import { ApiError } from '../../error';
import { Channels, ChannelFollowings } from '../../../../models';
import { genId } from '../../../../misc/gen-id';
+import { publishUserEvent } from '../../../../services/stream';
export const meta = {
tags: ['channels'],
@@ -36,10 +37,12 @@ export default define(meta, async (ps, user) => {
throw new ApiError(meta.errors.noSuchChannel);
}
- await ChannelFollowings.save({
+ await ChannelFollowings.insert({
id: genId(),
createdAt: new Date(),
followerId: user.id,
followeeId: channel.id,
});
+
+ publishUserEvent(user.id, 'followChannel', channel);
});
diff --git a/src/server/api/endpoints/channels/timeline.ts b/src/server/api/endpoints/channels/timeline.ts
index acb34f124d..00a7cd86d5 100644
--- a/src/server/api/endpoints/channels/timeline.ts
+++ b/src/server/api/endpoints/channels/timeline.ts
@@ -87,7 +87,11 @@ export default define(meta, async (ps, user) => {
//#region Construct query
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.channelId = :channelId', { channelId: channel.id })
- .leftJoinAndSelect('note.user', 'user')
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('note.channel', 'channel');
//#endregion
diff --git a/src/server/api/endpoints/channels/unfollow.ts b/src/server/api/endpoints/channels/unfollow.ts
index 8cab5c36a6..3eb0f1519b 100644
--- a/src/server/api/endpoints/channels/unfollow.ts
+++ b/src/server/api/endpoints/channels/unfollow.ts
@@ -3,6 +3,7 @@ import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { ApiError } from '../../error';
import { Channels, ChannelFollowings } from '../../../../models';
+import { publishUserEvent } from '../../../../services/stream';
export const meta = {
tags: ['channels'],
@@ -39,4 +40,6 @@ export default define(meta, async (ps, user) => {
followerId: user.id,
followeeId: channel.id,
});
+
+ publishUserEvent(user.id, 'unfollowChannel', channel);
});
diff --git a/src/server/api/endpoints/clips/add-note.ts b/src/server/api/endpoints/clips/add-note.ts
index 4f5cc649e3..ee6a117b2d 100644
--- a/src/server/api/endpoints/clips/add-note.ts
+++ b/src/server/api/endpoints/clips/add-note.ts
@@ -68,7 +68,7 @@ export default define(meta, async (ps, user) => {
throw new ApiError(meta.errors.alreadyClipped);
}
- await ClipNotes.save({
+ await ClipNotes.insert({
id: genId(),
noteId: note.id,
clipId: clip.id
diff --git a/src/server/api/endpoints/clips/notes.ts b/src/server/api/endpoints/clips/notes.ts
index 6a507e2036..676629c328 100644
--- a/src/server/api/endpoints/clips/notes.ts
+++ b/src/server/api/endpoints/clips/notes.ts
@@ -71,7 +71,11 @@ export default define(meta, async (ps, user) => {
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere(`note.id IN (${ clipQuery.getQuery() })`)
- .leftJoinAndSelect('note.user', 'user')
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
.setParameters(clipQuery.getParameters());
if (user) {
diff --git a/src/server/api/endpoints/i.ts b/src/server/api/endpoints/i.ts
index e5b65e0930..87f6ae778d 100644
--- a/src/server/api/endpoints/i.ts
+++ b/src/server/api/endpoints/i.ts
@@ -1,6 +1,5 @@
import define from '../define';
-import { RegistryItems, UserProfiles, Users } from '../../../models';
-import { genId } from '../../../misc/gen-id';
+import { Users } from '../../../models';
export const meta = {
desc: {
@@ -23,28 +22,8 @@ export const meta = {
export default define(meta, async (ps, user, token) => {
const isSecure = token == null;
- // TODO: そのうち消す
- const profile = await UserProfiles.findOneOrFail(user.id);
- for (const [k, v] of Object.entries(profile.clientData)) {
- await RegistryItems.insert({
- id: genId(),
- createdAt: new Date(),
- updatedAt: new Date(),
- userId: user.id,
- domain: null,
- scope: ['client', 'base'],
- key: k,
- value: v
- });
- }
- await UserProfiles.createQueryBuilder().update()
- .set({
- clientData: {},
- })
- .where('userId = :id', { id: user.id })
- .execute();
-
- return await Users.pack(user, user, {
+ // ここで渡ってきている user はキャッシュされていて古い可能性もあるので id だけ渡す
+ return await Users.pack(user.id, user, {
detail: true,
includeSecrets: isSecure
});
diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts
index 0e09bc73fd..812a4bd1dd 100644
--- a/src/server/api/endpoints/i/notifications.ts
+++ b/src/server/api/endpoints/i/notifications.ts
@@ -85,7 +85,13 @@ export default define(meta, async (ps, user) => {
const query = makePaginationQuery(Notifications.createQueryBuilder('notification'), ps.sinceId, ps.untilId)
.andWhere(`notification.notifieeId = :meId`, { meId: user.id })
- .leftJoinAndSelect('notification.notifier', 'notifier');
+ .leftJoinAndSelect('notification.notifier', 'notifier')
+ .leftJoinAndSelect('notification.note', 'note')
+ .leftJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser');
query.andWhere(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`);
query.setParameters(mutingQuery.getParameters());
@@ -110,5 +116,5 @@ export default define(meta, async (ps, user) => {
readNotification(user.id, notifications.map(x => x.id));
}
- return await Notifications.packMany(notifications);
+ return await Notifications.packMany(notifications, user.id);
});
diff --git a/src/server/api/endpoints/i/read-announcement.ts b/src/server/api/endpoints/i/read-announcement.ts
index 4a4a021af9..d6acb3d2e6 100644
--- a/src/server/api/endpoints/i/read-announcement.ts
+++ b/src/server/api/endpoints/i/read-announcement.ts
@@ -52,7 +52,7 @@ export default define(meta, async (ps, user) => {
}
// Create read
- await AnnouncementReads.save({
+ await AnnouncementReads.insert({
id: genId(),
createdAt: new Date(),
announcementId: ps.announcementId,
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index a1faf8f1c2..92be2e9e6d 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -1,6 +1,6 @@
import $ from 'cafy';
import { ID } from '../../../../misc/cafy-id';
-import { publishMainStream } from '../../../../services/stream';
+import { publishMainStream, publishUserEvent } from '../../../../services/stream';
import acceptAllFollowRequests from '../../../../services/following/requests/accept-all';
import { publishToFollowers } from '../../../../services/i/update';
import define from '../../define';
@@ -317,6 +317,7 @@ export default define(meta, async (ps, user, token) => {
// Publish meUpdated event
publishMainStream(user.id, 'meUpdated', iObj);
+ publishUserEvent(user.id, 'updateUserProfile', await UserProfiles.findOne(user.id));
// 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認
if (user.isLocked && ps.isLocked === false) {
diff --git a/src/server/api/endpoints/miauth/gen-token.ts b/src/server/api/endpoints/miauth/gen-token.ts
index 0634debb1e..401ed16389 100644
--- a/src/server/api/endpoints/miauth/gen-token.ts
+++ b/src/server/api/endpoints/miauth/gen-token.ts
@@ -52,7 +52,7 @@ export default define(meta, async (ps, user) => {
const now = new Date();
// Insert access token doc
- await AccessTokens.save({
+ await AccessTokens.insert({
id: genId(),
createdAt: now,
lastUsedAt: now,
diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts
index 437ad96107..ebfc6028ed 100644
--- a/src/server/api/endpoints/mute/create.ts
+++ b/src/server/api/endpoints/mute/create.ts
@@ -6,6 +6,7 @@ import { getUser } from '../../common/getters';
import { genId } from '../../../../misc/gen-id';
import { Mutings, NoteWatchings } from '../../../../models';
import { Muting } from '../../../../models/entities/muting';
+import { publishUserEvent } from '../../../../services/stream';
export const meta = {
desc: {
@@ -82,6 +83,8 @@ export default define(meta, async (ps, user) => {
muteeId: mutee.id,
} as Muting);
+ publishUserEvent(user.id, 'mute', mutee);
+
NoteWatchings.delete({
userId: muter.id,
noteUserId: mutee.id
diff --git a/src/server/api/endpoints/mute/delete.ts b/src/server/api/endpoints/mute/delete.ts
index 217352acb4..67a59e3ae4 100644
--- a/src/server/api/endpoints/mute/delete.ts
+++ b/src/server/api/endpoints/mute/delete.ts
@@ -4,6 +4,7 @@ import define from '../../define';
import { ApiError } from '../../error';
import { getUser } from '../../common/getters';
import { Mutings } from '../../../../models';
+import { publishUserEvent } from '../../../../services/stream';
export const meta = {
desc: {
@@ -76,4 +77,6 @@ export default define(meta, async (ps, user) => {
await Mutings.delete({
id: exist.id
});
+
+ publishUserEvent(user.id, 'unmute', mutee);
});
diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts
index fab8455d78..30e6e92fec 100644
--- a/src/server/api/endpoints/notes.ts
+++ b/src/server/api/endpoints/notes.ts
@@ -76,7 +76,11 @@ export default define(meta, async (ps) => {
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere(`note.visibility = 'public'`)
.andWhere(`note.localOnly = FALSE`)
- .leftJoinAndSelect('note.user', 'user');
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser');
if (ps.local) {
query.andWhere('note.userHost IS NULL');
diff --git a/src/server/api/endpoints/notes/children.ts b/src/server/api/endpoints/notes/children.ts
index 0875e0f240..072a25e024 100644
--- a/src/server/api/endpoints/notes/children.ts
+++ b/src/server/api/endpoints/notes/children.ts
@@ -64,7 +64,11 @@ export default define(meta, async (ps, user) => {
}));
}));
}))
- .leftJoinAndSelect('note.user', 'user');
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser');
generateVisibilityQuery(query, user);
if (user) generateMutedUserQuery(query, user);
diff --git a/src/server/api/endpoints/notes/favorites/create.ts b/src/server/api/endpoints/notes/favorites/create.ts
index 952bbfd0eb..d66ce37a46 100644
--- a/src/server/api/endpoints/notes/favorites/create.ts
+++ b/src/server/api/endpoints/notes/favorites/create.ts
@@ -61,7 +61,7 @@ export default define(meta, async (ps, user) => {
}
// Create favorite
- await NoteFavorites.save({
+ await NoteFavorites.insert({
id: genId(),
createdAt: new Date(),
noteId: note.id,
diff --git a/src/server/api/endpoints/notes/featured.ts b/src/server/api/endpoints/notes/featured.ts
index 4dda7d0edb..b3dffa4272 100644
--- a/src/server/api/endpoints/notes/featured.ts
+++ b/src/server/api/endpoints/notes/featured.ts
@@ -49,7 +49,11 @@ export default define(meta, async (ps, user) => {
.andWhere(`note.score > 0`)
.andWhere(`note.createdAt > :date`, { date: new Date(Date.now() - day) })
.andWhere(`note.visibility = 'public'`)
- .leftJoinAndSelect('note.user', 'user');
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser');
if (user) generateMutedUserQuery(query, user);
diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts
index 6d99f1fdbc..64fc3cbf6c 100644
--- a/src/server/api/endpoints/notes/global-timeline.ts
+++ b/src/server/api/endpoints/notes/global-timeline.ts
@@ -8,8 +8,6 @@ import { Notes } from '../../../../models';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
import { activeUsersChart } from '../../../../services/chart';
import { generateRepliesQuery } from '../../common/generate-replies-query';
-import { injectPromo } from '../../common/inject-promo';
-import { injectFeatured } from '../../common/inject-featured';
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
export const meta = {
@@ -81,7 +79,11 @@ export default define(meta, async (ps, user) => {
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.visibility = \'public\'')
.andWhere('note.channelId IS NULL')
- .leftJoinAndSelect('note.user', 'user');
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser');
generateRepliesQuery(query, user);
if (user) generateMutedUserQuery(query, user);
@@ -94,9 +96,6 @@ export default define(meta, async (ps, user) => {
const timeline = await query.take(ps.limit!).getMany();
- await injectPromo(timeline, user);
- await injectFeatured(timeline, user);
-
process.nextTick(() => {
if (user) {
activeUsersChart.update(user);
diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts
index 2b91b8c67b..19c4593f5b 100644
--- a/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -10,8 +10,6 @@ import { generateVisibilityQuery } from '../../common/generate-visibility-query'
import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
import { activeUsersChart } from '../../../../services/chart';
import { generateRepliesQuery } from '../../common/generate-replies-query';
-import { injectPromo } from '../../common/inject-promo';
-import { injectFeatured } from '../../common/inject-featured';
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
import { generateChannelQuery } from '../../common/generate-channel-query';
@@ -129,7 +127,11 @@ export default define(meta, async (ps, user) => {
qb.where(`((note.userId IN (${ followingQuery.getQuery() })) OR (note.userId = :meId))`, { meId: user.id })
.orWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)');
}))
- .leftJoinAndSelect('note.user', 'user')
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
.setParameters(followingQuery.getParameters());
generateChannelQuery(query, user);
@@ -175,9 +177,6 @@ export default define(meta, async (ps, user) => {
const timeline = await query.take(ps.limit!).getMany();
- await injectPromo(timeline, user);
- await injectFeatured(timeline, user);
-
process.nextTick(() => {
if (user) {
activeUsersChart.update(user);
diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts
index 51e35e6241..546d3619f7 100644
--- a/src/server/api/endpoints/notes/local-timeline.ts
+++ b/src/server/api/endpoints/notes/local-timeline.ts
@@ -10,8 +10,6 @@ import { generateVisibilityQuery } from '../../common/generate-visibility-query'
import { activeUsersChart } from '../../../../services/chart';
import { Brackets } from 'typeorm';
import { generateRepliesQuery } from '../../common/generate-replies-query';
-import { injectPromo } from '../../common/inject-promo';
-import { injectFeatured } from '../../common/inject-featured';
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
import { generateChannelQuery } from '../../common/generate-channel-query';
@@ -98,7 +96,11 @@ export default define(meta, async (ps, user) => {
const query = makePaginationQuery(Notes.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('(note.visibility = \'public\') AND (note.userHost IS NULL)')
- .leftJoinAndSelect('note.user', 'user');
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser');
generateChannelQuery(query, user);
generateRepliesQuery(query, user);
@@ -128,9 +130,6 @@ export default define(meta, async (ps, user) => {
const timeline = await query.take(ps.limit!).getMany();
- await injectPromo(timeline, user);
- await injectFeatured(timeline, user);
-
process.nextTick(() => {
if (user) {
activeUsersChart.update(user);
diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts
index 8a9d295d38..30368ea578 100644
--- a/src/server/api/endpoints/notes/mentions.ts
+++ b/src/server/api/endpoints/notes/mentions.ts
@@ -63,7 +63,11 @@ export default define(meta, async (ps, user) => {
.where(`:meId = ANY(note.mentions)`, { meId: user.id })
.orWhere(`:meId = ANY(note.visibleUserIds)`, { meId: user.id });
}))
- .leftJoinAndSelect('note.user', 'user');
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser');
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);
@@ -79,9 +83,7 @@ export default define(meta, async (ps, user) => {
const mentions = await query.take(ps.limit!).getMany();
- for (const note of mentions) {
- read(user.id, note.id);
- }
+ read(user.id, mentions.map(note => note.id));
return await Notes.packMany(mentions, user);
});
diff --git a/src/server/api/endpoints/notes/renotes.ts b/src/server/api/endpoints/notes/renotes.ts
index 31c24f294a..dcda213918 100644
--- a/src/server/api/endpoints/notes/renotes.ts
+++ b/src/server/api/endpoints/notes/renotes.ts
@@ -68,7 +68,11 @@ export default define(meta, async (ps, user) => {
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere(`note.renoteId = :renoteId`, { renoteId: note.id })
- .leftJoinAndSelect('note.user', 'user');
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser');
generateVisibilityQuery(query, user);
if (user) generateMutedUserQuery(query, user);
diff --git a/src/server/api/endpoints/notes/replies.ts b/src/server/api/endpoints/notes/replies.ts
index 9fad74c78e..6f33e2f233 100644
--- a/src/server/api/endpoints/notes/replies.ts
+++ b/src/server/api/endpoints/notes/replies.ts
@@ -59,7 +59,11 @@ export const meta = {
export default define(meta, async (ps, user) => {
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere('note.replyId = :replyId', { replyId: ps.noteId })
- .leftJoinAndSelect('note.user', 'user');
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser');
generateVisibilityQuery(query, user);
if (user) generateMutedUserQuery(query, user);
diff --git a/src/server/api/endpoints/notes/search-by-tag.ts b/src/server/api/endpoints/notes/search-by-tag.ts
index e0f7f4d62c..47b41d9294 100644
--- a/src/server/api/endpoints/notes/search-by-tag.ts
+++ b/src/server/api/endpoints/notes/search-by-tag.ts
@@ -95,7 +95,11 @@ export const meta = {
export default define(meta, async (ps, me) => {
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
- .leftJoinAndSelect('note.user', 'user');
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser');
generateVisibilityQuery(query, me);
if (me) generateMutedUserQuery(query, me);
diff --git a/src/server/api/endpoints/notes/search.ts b/src/server/api/endpoints/notes/search.ts
index 1aca056299..230d2b0294 100644
--- a/src/server/api/endpoints/notes/search.ts
+++ b/src/server/api/endpoints/notes/search.ts
@@ -79,7 +79,11 @@ export default define(meta, async (ps, me) => {
query
.andWhere('note.text ILIKE :q', { q: `%${ps.query}%` })
- .leftJoinAndSelect('note.user', 'user');
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser');
generateVisibilityQuery(query, me);
if (me) generateMutedUserQuery(query, me);
diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts
index f09f3d1733..d025944cc2 100644
--- a/src/server/api/endpoints/notes/timeline.ts
+++ b/src/server/api/endpoints/notes/timeline.ts
@@ -8,8 +8,6 @@ import { generateMutedUserQuery } from '../../common/generate-muted-user-query';
import { activeUsersChart } from '../../../../services/chart';
import { Brackets } from 'typeorm';
import { generateRepliesQuery } from '../../common/generate-replies-query';
-import { injectPromo } from '../../common/inject-promo';
-import { injectFeatured } from '../../common/inject-featured';
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query';
import { generateChannelQuery } from '../../common/generate-channel-query';
@@ -122,7 +120,11 @@ export default define(meta, async (ps, user) => {
.where('note.userId = :meId', { meId: user.id });
if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`);
}))
- .leftJoinAndSelect('note.user', 'user')
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
.setParameters(followingQuery.getParameters());
generateChannelQuery(query, user);
@@ -168,9 +170,6 @@ export default define(meta, async (ps, user) => {
const timeline = await query.take(ps.limit!).getMany();
- await injectPromo(timeline, user);
- await injectFeatured(timeline, user);
-
process.nextTick(() => {
if (user) {
activeUsersChart.update(user);
diff --git a/src/server/api/endpoints/notes/user-list-timeline.ts b/src/server/api/endpoints/notes/user-list-timeline.ts
index b0ff499d95..9ffb38bddc 100644
--- a/src/server/api/endpoints/notes/user-list-timeline.ts
+++ b/src/server/api/endpoints/notes/user-list-timeline.ts
@@ -130,7 +130,11 @@ export default define(meta, async (ps, user) => {
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere(`note.userId IN (${ listQuery.getQuery() })`)
- .leftJoinAndSelect('note.user', 'user')
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser')
.setParameters(listQuery.getParameters());
generateVisibilityQuery(query, user);
diff --git a/src/server/api/endpoints/pages/like.ts b/src/server/api/endpoints/pages/like.ts
index 5c7e13f1c8..3fc2b6ca23 100644
--- a/src/server/api/endpoints/pages/like.ts
+++ b/src/server/api/endpoints/pages/like.ts
@@ -68,7 +68,7 @@ export default define(meta, async (ps, user) => {
}
// Create like
- await PageLikes.save({
+ await PageLikes.insert({
id: genId(),
createdAt: new Date(),
pageId: page.id,
diff --git a/src/server/api/endpoints/promo/read.ts b/src/server/api/endpoints/promo/read.ts
index 57eb0681e5..63c90e5d7f 100644
--- a/src/server/api/endpoints/promo/read.ts
+++ b/src/server/api/endpoints/promo/read.ts
@@ -46,7 +46,7 @@ export default define(meta, async (ps, user) => {
return;
}
- await PromoReads.save({
+ await PromoReads.insert({
id: genId(),
createdAt: new Date(),
noteId: note.id,
diff --git a/src/server/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts
index ceb70a9274..9fc70b5609 100644
--- a/src/server/api/endpoints/sw/register.ts
+++ b/src/server/api/endpoints/sw/register.ts
@@ -58,7 +58,7 @@ export default define(meta, async (ps, user) => {
};
}
- await SwSubscriptions.save({
+ await SwSubscriptions.insert({
id: genId(),
createdAt: new Date(),
userId: user.id,
diff --git a/src/server/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts
index bd4a2739c6..fb83d7beb8 100644
--- a/src/server/api/endpoints/users/followers.ts
+++ b/src/server/api/endpoints/users/followers.ts
@@ -76,7 +76,8 @@ export default define(meta, async (ps, me) => {
}
const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
- .andWhere(`following.followeeId = :userId`, { userId: user.id });
+ .andWhere(`following.followeeId = :userId`, { userId: user.id })
+ .innerJoinAndSelect('following.follower', 'follower');
const followings = await query
.take(ps.limit!)
diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts
index 9efb8bfc93..d5e8dc1f92 100644
--- a/src/server/api/endpoints/users/following.ts
+++ b/src/server/api/endpoints/users/following.ts
@@ -76,7 +76,8 @@ export default define(meta, async (ps, me) => {
}
const query = makePaginationQuery(Followings.createQueryBuilder('following'), ps.sinceId, ps.untilId)
- .andWhere(`following.followerId = :userId`, { userId: user.id });
+ .andWhere(`following.followerId = :userId`, { userId: user.id })
+ .innerJoinAndSelect('following.followee', 'followee');
const followings = await query
.take(ps.limit!)
diff --git a/src/server/api/endpoints/users/groups/create.ts b/src/server/api/endpoints/users/groups/create.ts
index ca011d5cd6..78d2714874 100644
--- a/src/server/api/endpoints/users/groups/create.ts
+++ b/src/server/api/endpoints/users/groups/create.ts
@@ -39,7 +39,7 @@ export default define(meta, async (ps, user) => {
} as UserGroup);
// Push the owner
- await UserGroupJoinings.save({
+ await UserGroupJoinings.insert({
id: genId(),
createdAt: new Date(),
userId: user.id,
diff --git a/src/server/api/endpoints/users/groups/invitations/accept.ts b/src/server/api/endpoints/users/groups/invitations/accept.ts
index e86709f83b..2fa22bcf7e 100644
--- a/src/server/api/endpoints/users/groups/invitations/accept.ts
+++ b/src/server/api/endpoints/users/groups/invitations/accept.ts
@@ -52,7 +52,7 @@ export default define(meta, async (ps, user) => {
}
// Push the user
- await UserGroupJoinings.save({
+ await UserGroupJoinings.insert({
id: genId(),
createdAt: new Date(),
userId: user.id,
diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts
index 33e3ecb03f..fc5998c378 100644
--- a/src/server/api/endpoints/users/notes.ts
+++ b/src/server/api/endpoints/users/notes.ts
@@ -131,7 +131,11 @@ export default define(meta, async (ps, me) => {
//#region Construct query
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.userId = :userId', { userId: user.id })
- .leftJoinAndSelect('note.user', 'user');
+ .innerJoinAndSelect('note.user', 'user')
+ .leftJoinAndSelect('note.reply', 'reply')
+ .leftJoinAndSelect('note.renote', 'renote')
+ .leftJoinAndSelect('reply.user', 'replyUser')
+ .leftJoinAndSelect('renote.user', 'renoteUser');
generateVisibilityQuery(query, me);
if (me) generateMutedUserQuery(query, me, user);
diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
index 7a5efc6cc9..d8f2e6d516 100644
--- a/src/server/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -53,7 +53,7 @@ export default async (ctx: Koa.Context) => {
async function fail(status?: number, failure?: { error: string }) {
// Append signin history
- await Signins.save({
+ await Signins.insert({
id: genId(),
createdAt: new Date(),
userId: user.id,
@@ -198,7 +198,7 @@ export default async (ctx: Koa.Context) => {
const challengeId = genId();
- await AttestationChallenges.save({
+ await AttestationChallenges.insert({
userId: user.id,
id: challengeId,
challenge: hash(Buffer.from(challenge, 'utf-8')).toString('hex'),
diff --git a/src/server/api/stream/channels/antenna.ts b/src/server/api/stream/channels/antenna.ts
index b5a792f814..36a474f2ac 100644
--- a/src/server/api/stream/channels/antenna.ts
+++ b/src/server/api/stream/channels/antenna.ts
@@ -27,6 +27,8 @@ export default class extends Channel {
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isMutedUserRelated(note, this.muting)) return;
+ this.connection.cacheNote(note);
+
this.send('note', note);
} else {
this.send(type, body);
diff --git a/src/server/api/stream/channels/channel.ts b/src/server/api/stream/channels/channel.ts
index aa570d1ef4..47a52465b2 100644
--- a/src/server/api/stream/channels/channel.ts
+++ b/src/server/api/stream/channels/channel.ts
@@ -43,6 +43,8 @@ export default class extends Channel {
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isMutedUserRelated(note, this.muting)) return;
+ this.connection.cacheNote(note);
+
this.send('note', note);
}
diff --git a/src/server/api/stream/channels/global-timeline.ts b/src/server/api/stream/channels/global-timeline.ts
index 8c97e67226..8353f45323 100644
--- a/src/server/api/stream/channels/global-timeline.ts
+++ b/src/server/api/stream/channels/global-timeline.ts
@@ -56,6 +56,8 @@ export default class extends Channel {
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
+ this.connection.cacheNote(note);
+
this.send('note', note);
}
diff --git a/src/server/api/stream/channels/hashtag.ts b/src/server/api/stream/channels/hashtag.ts
index 41447039d5..1b7f8efcc1 100644
--- a/src/server/api/stream/channels/hashtag.ts
+++ b/src/server/api/stream/channels/hashtag.ts
@@ -37,6 +37,8 @@ export default class extends Channel {
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isMutedUserRelated(note, this.muting)) return;
+ this.connection.cacheNote(note);
+
this.send('note', note);
}
diff --git a/src/server/api/stream/channels/home-timeline.ts b/src/server/api/stream/channels/home-timeline.ts
index 6cfa6eae7b..59ba31c316 100644
--- a/src/server/api/stream/channels/home-timeline.ts
+++ b/src/server/api/stream/channels/home-timeline.ts
@@ -64,6 +64,8 @@ export default class extends Channel {
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
+ this.connection.cacheNote(note);
+
this.send('note', note);
}
diff --git a/src/server/api/stream/channels/hybrid-timeline.ts b/src/server/api/stream/channels/hybrid-timeline.ts
index a9e577cacb..9715e9973f 100644
--- a/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/src/server/api/stream/channels/hybrid-timeline.ts
@@ -73,6 +73,8 @@ export default class extends Channel {
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
+ this.connection.cacheNote(note);
+
this.send('note', note);
}
diff --git a/src/server/api/stream/channels/local-timeline.ts b/src/server/api/stream/channels/local-timeline.ts
index a3a5e491fc..e159c72d60 100644
--- a/src/server/api/stream/channels/local-timeline.ts
+++ b/src/server/api/stream/channels/local-timeline.ts
@@ -58,6 +58,8 @@ export default class extends Channel {
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
+ this.connection.cacheNote(note);
+
this.send('note', note);
}
diff --git a/src/server/api/stream/channels/main.ts b/src/server/api/stream/channels/main.ts
index b69c2ec355..780bc0b89f 100644
--- a/src/server/api/stream/channels/main.ts
+++ b/src/server/api/stream/channels/main.ts
@@ -18,18 +18,22 @@ export default class extends Channel {
case 'notification': {
if (this.muting.has(body.userId)) return;
if (body.note && body.note.isHidden) {
- body.note = await Notes.pack(body.note.id, this.user, {
+ const note = await Notes.pack(body.note.id, this.user, {
detail: true
});
+ this.connection.cacheNote(note);
+ body.note = note;
}
break;
}
case 'mention': {
if (this.muting.has(body.userId)) return;
if (body.isHidden) {
- body = await Notes.pack(body.id, this.user, {
+ const note = await Notes.pack(body.id, this.user, {
detail: true
});
+ this.connection.cacheNote(note);
+ body = note;
}
break;
}
diff --git a/src/server/api/stream/index.ts b/src/server/api/stream/index.ts
index c56a0a157b..99ae558696 100644
--- a/src/server/api/stream/index.ts
+++ b/src/server/api/stream/index.ts
@@ -14,6 +14,7 @@ import { AccessToken } from '../../../models/entities/access-token';
import { UserProfile } from '../../../models/entities/user-profile';
import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '../../../services/stream';
import { UserGroup } from '../../../models/entities/user-group';
+import { PackedNote } from '../../../models/repositories/note';
/**
* Main stream connection
@@ -29,10 +30,7 @@ export default class Connection {
public subscriber: EventEmitter;
private channels: Channel[] = [];
private subscribingNotes: any = {};
- private followingClock: ReturnType<typeof setInterval>;
- private mutingClock: ReturnType<typeof setInterval>;
- private followingChannelsClock: ReturnType<typeof setInterval>;
- private userProfileClock: ReturnType<typeof setInterval>;
+ private cachedNotes: PackedNote[] = [];
constructor(
wsConnection: websocket.connection,
@@ -53,16 +51,49 @@ export default class Connection {
if (this.user) {
this.updateFollowing();
- this.followingClock = setInterval(this.updateFollowing, 5000);
-
this.updateMuting();
- this.mutingClock = setInterval(this.updateMuting, 5000);
-
this.updateFollowingChannels();
- this.followingChannelsClock = setInterval(this.updateFollowingChannels, 5000);
-
this.updateUserProfile();
- this.userProfileClock = setInterval(this.updateUserProfile, 5000);
+
+ this.subscriber.on(`user:${this.user.id}`, ({ type, body }) => {
+ this.onUserEvent(type, body);
+ });
+ }
+ }
+
+ @autobind
+ private onUserEvent(type: string, body: any) {
+ switch (type) {
+ case 'follow':
+ this.following.add(body.id);
+ break;
+
+ case 'unfollow':
+ this.following.delete(body.id);
+ break;
+
+ case 'mute':
+ this.muting.add(body.id);
+ break;
+
+ case 'unmute':
+ this.muting.delete(body.id);
+ break;
+
+ case 'followChannel':
+ this.followingChannels.add(body.id);
+ break;
+
+ case 'unfollowChannel':
+ this.followingChannels.delete(body.id);
+ break;
+
+ case 'updateUserProfile':
+ this.userProfile = body;
+ break;
+
+ default:
+ break;
}
}
@@ -86,9 +117,9 @@ export default class Connection {
switch (type) {
case 'api': this.onApiRequest(body); break;
case 'readNotification': this.onReadNotification(body); break;
- case 'subNote': this.onSubscribeNote(body, true); break;
- case 'sn': this.onSubscribeNote(body, true); break; // alias
- case 's': this.onSubscribeNote(body, false); break;
+ case 'subNote': this.onSubscribeNote(body); break;
+ case 's': this.onSubscribeNote(body); break; // alias
+ case 'sr': this.onSubscribeNote(body); this.readNote(body); break;
case 'unsubNote': this.onUnsubscribeNote(body); break;
case 'un': this.onUnsubscribeNote(body); break; // alias
case 'connect': this.onChannelConnectRequested(body); break;
@@ -109,6 +140,48 @@ export default class Connection {
this.sendMessageToWs(type, body);
}
+ @autobind
+ public cacheNote(note: PackedNote) {
+ const add = (note: PackedNote) => {
+ const existIndex = this.cachedNotes.findIndex(n => n.id === note.id);
+ if (existIndex > -1) {
+ this.cachedNotes[existIndex] = note;
+ return;
+ }
+
+ this.cachedNotes.unshift(note);
+ if (this.cachedNotes.length > 32) {
+ this.cachedNotes.splice(32);
+ }
+ };
+
+ add(note);
+ if (note.reply) add(note.reply);
+ if (note.renote) add(note.renote);
+ }
+
+ @autobind
+ private readNote(body: any) {
+ const id = body.id;
+
+ const note = this.cachedNotes.find(n => n.id === id);
+ if (note == null) return;
+
+ if (this.user && (note.userId !== this.user.id)) {
+ if (note.mentions && note.mentions.includes(this.user.id)) {
+ readNote(this.user.id, [note]);
+ } else if (note.visibleUserIds && note.visibleUserIds.includes(this.user.id)) {
+ readNote(this.user.id, [note]);
+ }
+
+ if (this.followingChannels.has(note.channelId)) {
+ // TODO
+ }
+
+ // TODO: アンテナの既読処理
+ }
+ }
+
/**
* APIリクエスト要求時
*/
@@ -145,7 +218,7 @@ export default class Connection {
* 投稿購読要求時
*/
@autobind
- private onSubscribeNote(payload: any, read: boolean) {
+ private onSubscribeNote(payload: any) {
if (!payload.id) return;
if (this.subscribingNotes[payload.id] == null) {
@@ -157,10 +230,6 @@ export default class Connection {
if (this.subscribingNotes[payload.id] === 1) {
this.subscriber.on(`noteStream:${payload.id}`, this.onNoteStreamMessage);
}
-
- if (this.user && read) {
- readNote(this.user.id, payload.id);
- }
}
/**
@@ -335,10 +404,5 @@ export default class Connection {
for (const c of this.channels.filter(c => c.dispose)) {
if (c.dispose) c.dispose();
}
-
- if (this.followingClock) clearInterval(this.followingClock);
- if (this.mutingClock) clearInterval(this.mutingClock);
- if (this.followingChannelsClock) clearInterval(this.followingChannelsClock);
- if (this.userProfileClock) clearInterval(this.userProfileClock);
}
}
diff --git a/src/server/web/index.ts b/src/server/web/index.ts
index 7b0b82eedf..ea356206ff 100644
--- a/src/server/web/index.ts
+++ b/src/server/web/index.ts
@@ -29,6 +29,7 @@ const markdown = MarkdownIt({
});
const staticAssets = `${__dirname}/../../../assets/`;
+const docAssets = `${__dirname}/../../../src/docs/`;
const assets = `${__dirname}/../../assets/`;
// Init app
@@ -44,7 +45,7 @@ app.use(views(__dirname + '/views', {
}));
// Serve favicon
-app.use(favicon(`${__dirname}/../../../assets/favicon.png`));
+app.use(favicon(`${__dirname}/../../../assets/favicon.ico`));
// Common request handler
app.use(async (ctx, next) => {
@@ -65,6 +66,13 @@ router.get('/static-assets/(.*)', async ctx => {
});
});
+router.get('/doc-assets/(.*)', async ctx => {
+ await send(ctx as any, ctx.path.replace('/doc-assets/', ''), {
+ root: docAssets,
+ maxage: ms('7 days'),
+ });
+});
+
router.get('/assets/(.*)', async ctx => {
await send(ctx as any, ctx.path.replace('/assets/', ''), {
root: assets,
@@ -75,7 +83,7 @@ router.get('/assets/(.*)', async ctx => {
// Apple touch icon
router.get('/apple-touch-icon.png', async ctx => {
await send(ctx as any, '/apple-touch-icon.png', {
- root: assets
+ root: staticAssets
});
});
diff --git a/src/server/web/views/flush.pug b/src/server/web/views/flush.pug
index 59fed1f15d..ec585a34db 100644
--- a/src/server/web/views/flush.pug
+++ b/src/server/web/views/flush.pug
@@ -4,35 +4,44 @@ html
#msg
script.
const msg = document.getElementById('msg');
+ const successText = `\nSuccess Flush! <a href="/">Back to Misskey</a>\n成功しました。<a href="/">Misskeyを開き直してください。</a>`;
- try {
- localStorage.clear();
- message('localStorage cleared');
+ message('Start flushing.');
- const delidb = indexedDB.deleteDatabase('MisskeyClient');
- delidb.onsuccess = () => message('indexedDB cleared');
+ (async function() {
+ try {
+ localStorage.clear();
+ message('localStorage cleared.');
- if (navigator.serviceWorker.controller) {
- navigator.serviceWorker.controller.postMessage('clear');
- navigator.serviceWorker.getRegistrations()
- .then(registrations => {
- return Promise.all(registrations.map(registration => registration.unregister()));
- })
- .then(() => {
- message('Success Flush! Please reopen Misskey.\n成功しました。Misskeyを開き直してください。');
- })
- .catch(e => { throw Error(e) });
- } else {
- message('Success Flush! Please reopen Misskey.\n成功しました。Misskeyを開き直してください。');
+ const idbPromises = ['MisskeyClient', 'keyval-store'].map((name, i, arr) => new Promise((res, rej) => {
+ const delidb = indexedDB.deleteDatabase(name);
+ delidb.onsuccess = () => res(message(`indexedDB "${name}" cleared. (${i + 1}/${arr.length})`));
+ delidb.onerror = e => rej(e)
+ }));
+
+ await Promise.all(idbPromises);
+
+ if (navigator.serviceWorker.controller) {
+ navigator.serviceWorker.controller.postMessage('clear');
+ await navigator.serviceWorker.getRegistrations()
+ .then(registrations => {
+ return Promise.all(registrations.map(registration => registration.unregister()));
+ })
+ .catch(e => { throw Error(e) });
+ }
+
+ message(successText);
+ } catch (e) {
+ message(`\n${e}\n\nFlush Failed. <a href="/flush">Please retry.</a>\n失敗しました。<a href="/flush">もう一度試してみてください。</a>`);
+ message(`\nIf you retry more than 3 times, clear the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`)
+
+ console.error(e);
+ setTimeout(() => {
+ location = '/';
+ }, 10000)
}
- } catch (e) {
- console.error(e);
- message(`${e}¥n¥nFlush Failed. Please reopen Misskey.\n失敗しました。Misskeyを開き直してください。`);
- setTimeout(() => {
- location = '/';
- }, 10000)
- }
+ })();
function message(text) {
- msg.insertAdjacentHTML('beforeend', `<p>[${(new Date()).toString()}] ${text.replace(/¥n/g,'<br>')}</p>`)
+ msg.insertAdjacentHTML('beforeend', `<p>[${(new Date()).toString()}] ${text.replace(/\n/g,'<br>')}</p>`)
}
diff --git a/src/services/add-note-to-antenna.ts b/src/services/add-note-to-antenna.ts
index 2c893488c3..3ba3d1eef5 100644
--- a/src/services/add-note-to-antenna.ts
+++ b/src/services/add-note-to-antenna.ts
@@ -10,7 +10,7 @@ export async function addNoteToAntenna(antenna: Antenna, note: Note, noteUser: U
// 通知しない設定になっているか、自分自身の投稿なら既読にする
const read = !antenna.notify || (antenna.userId === noteUser.id);
- AntennaNotes.save({
+ AntennaNotes.insert({
id: genId(),
antennaId: antenna.id,
noteId: note.id,
diff --git a/src/services/blocking/create.ts b/src/services/blocking/create.ts
index def4f33585..dec48d26de 100644
--- a/src/services/blocking/create.ts
+++ b/src/services/blocking/create.ts
@@ -1,4 +1,4 @@
-import { publishMainStream } from '../stream';
+import { publishMainStream, publishUserEvent } from '../stream';
import { renderActivity } from '../../remote/activitypub/renderer';
import renderFollow from '../../remote/activitypub/renderer/follow';
import renderUndo from '../../remote/activitypub/renderer/undo';
@@ -18,7 +18,7 @@ export default async function(blocker: User, blockee: User) {
unFollow(blockee, blocker)
]);
- await Blockings.save({
+ await Blockings.insert({
id: genId(),
createdAt: new Date(),
blockerId: blocker.id,
@@ -55,7 +55,10 @@ async function cancelRequest(follower: User, followee: User) {
if (Users.isLocalUser(follower)) {
Users.pack(followee, follower, {
detail: true
- }).then(packed => publishMainStream(follower.id, 'unfollow', packed));
+ }).then(packed => {
+ publishUserEvent(follower.id, 'unfollow', packed);
+ publishMainStream(follower.id, 'unfollow', packed);
+ });
}
// リモートにフォローリクエストをしていたらUndoFollow送信
@@ -97,7 +100,10 @@ async function unFollow(follower: User, followee: User) {
if (Users.isLocalUser(follower)) {
Users.pack(followee, follower, {
detail: true
- }).then(packed => publishMainStream(follower.id, 'unfollow', packed));
+ }).then(packed => {
+ publishUserEvent(follower.id, 'unfollow', packed);
+ publishMainStream(follower.id, 'unfollow', packed);
+ });
}
// リモートにフォローをしていたらUndoFollow送信
diff --git a/src/services/chart/charts/classes/active-users.ts b/src/services/chart/charts/classes/active-users.ts
index 5128150de6..4820f8281b 100644
--- a/src/services/chart/charts/classes/active-users.ts
+++ b/src/services/chart/charts/classes/active-users.ts
@@ -18,6 +18,18 @@ export default class ActiveUsersChart extends Chart<ActiveUsersLog> {
}
@autobind
+ protected aggregate(logs: ActiveUsersLog[]): ActiveUsersLog {
+ return {
+ local: {
+ users: logs.reduce((a, b) => a.concat(b.local.users), [] as ActiveUsersLog['local']['users']),
+ },
+ remote: {
+ users: logs.reduce((a, b) => a.concat(b.remote.users), [] as ActiveUsersLog['remote']['users']),
+ },
+ };
+ }
+
+ @autobind
protected async fetchActual(): Promise<DeepPartial<ActiveUsersLog>> {
return {};
}
@@ -25,11 +37,11 @@ export default class ActiveUsersChart extends Chart<ActiveUsersLog> {
@autobind
public async update(user: User) {
const update: Obj = {
- count: 1
+ users: [user.id]
};
- await this.incIfUnique({
+ await this.inc({
[Users.isLocalUser(user) ? 'local' : 'remote']: update
- }, 'users', user.id);
+ });
}
}
diff --git a/src/services/chart/charts/classes/drive.ts b/src/services/chart/charts/classes/drive.ts
index 57bb120beb..46399a34d8 100644
--- a/src/services/chart/charts/classes/drive.ts
+++ b/src/services/chart/charts/classes/drive.ts
@@ -28,6 +28,28 @@ export default class DriveChart extends Chart<DriveLog> {
}
@autobind
+ protected aggregate(logs: DriveLog[]): DriveLog {
+ return {
+ local: {
+ totalCount: logs[0].local.totalCount,
+ totalSize: logs[0].local.totalSize,
+ incCount: logs.reduce((a, b) => a + b.local.incCount, 0),
+ incSize: logs.reduce((a, b) => a + b.local.incSize, 0),
+ decCount: logs.reduce((a, b) => a + b.local.decCount, 0),
+ decSize: logs.reduce((a, b) => a + b.local.decSize, 0),
+ },
+ remote: {
+ totalCount: logs[0].remote.totalCount,
+ totalSize: logs[0].remote.totalSize,
+ incCount: logs.reduce((a, b) => a + b.remote.incCount, 0),
+ incSize: logs.reduce((a, b) => a + b.remote.incSize, 0),
+ decCount: logs.reduce((a, b) => a + b.remote.decCount, 0),
+ decSize: logs.reduce((a, b) => a + b.remote.decSize, 0),
+ },
+ };
+ }
+
+ @autobind
protected async fetchActual(): Promise<DeepPartial<DriveLog>> {
const [localCount, remoteCount, localSize, remoteSize] = await Promise.all([
DriveFiles.count({ userHost: null }),
diff --git a/src/services/chart/charts/classes/federation.ts b/src/services/chart/charts/classes/federation.ts
index bd2c497e7b..ab6ec2d4dd 100644
--- a/src/services/chart/charts/classes/federation.ts
+++ b/src/services/chart/charts/classes/federation.ts
@@ -21,6 +21,17 @@ export default class FederationChart extends Chart<FederationLog> {
}
@autobind
+ protected aggregate(logs: FederationLog[]): FederationLog {
+ return {
+ instance: {
+ total: logs[0].instance.total,
+ inc: logs.reduce((a, b) => a + b.instance.inc, 0),
+ dec: logs.reduce((a, b) => a + b.instance.dec, 0),
+ },
+ };
+ }
+
+ @autobind
protected async fetchActual(): Promise<DeepPartial<FederationLog>> {
const [total] = await Promise.all([
Instances.count({})
diff --git a/src/services/chart/charts/classes/hashtag.ts b/src/services/chart/charts/classes/hashtag.ts
index 38c3a94f0c..43db5b0a83 100644
--- a/src/services/chart/charts/classes/hashtag.ts
+++ b/src/services/chart/charts/classes/hashtag.ts
@@ -18,6 +18,18 @@ export default class HashtagChart extends Chart<HashtagLog> {
}
@autobind
+ protected aggregate(logs: HashtagLog[]): HashtagLog {
+ return {
+ local: {
+ users: logs.reduce((a, b) => a.concat(b.local.users), [] as HashtagLog['local']['users']),
+ },
+ remote: {
+ users: logs.reduce((a, b) => a.concat(b.remote.users), [] as HashtagLog['remote']['users']),
+ },
+ };
+ }
+
+ @autobind
protected async fetchActual(): Promise<DeepPartial<HashtagLog>> {
return {};
}
@@ -25,11 +37,11 @@ export default class HashtagChart extends Chart<HashtagLog> {
@autobind
public async update(hashtag: string, user: User) {
const update: Obj = {
- count: 1
+ users: [user.id]
};
- await this.incIfUnique({
+ await this.inc({
[Users.isLocalUser(user) ? 'local' : 'remote']: update
- }, 'users', user.id, hashtag);
+ }, hashtag);
}
}
diff --git a/src/services/chart/charts/classes/instance.ts b/src/services/chart/charts/classes/instance.ts
index 7575abfb6f..c32b864d87 100644
--- a/src/services/chart/charts/classes/instance.ts
+++ b/src/services/chart/charts/classes/instance.ts
@@ -37,6 +37,50 @@ export default class InstanceChart extends Chart<InstanceLog> {
}
@autobind
+ protected aggregate(logs: InstanceLog[]): InstanceLog {
+ return {
+ requests: {
+ failed: logs.reduce((a, b) => a + b.requests.failed, 0),
+ succeeded: logs.reduce((a, b) => a + b.requests.succeeded, 0),
+ received: logs.reduce((a, b) => a + b.requests.received, 0),
+ },
+ notes: {
+ total: logs[0].notes.total,
+ inc: logs.reduce((a, b) => a + b.notes.inc, 0),
+ dec: logs.reduce((a, b) => a + b.notes.dec, 0),
+ diffs: {
+ reply: logs.reduce((a, b) => a + b.notes.diffs.reply, 0),
+ renote: logs.reduce((a, b) => a + b.notes.diffs.renote, 0),
+ normal: logs.reduce((a, b) => a + b.notes.diffs.normal, 0),
+ },
+ },
+ users: {
+ total: logs[0].users.total,
+ inc: logs.reduce((a, b) => a + b.users.inc, 0),
+ dec: logs.reduce((a, b) => a + b.users.dec, 0),
+ },
+ following: {
+ total: logs[0].following.total,
+ inc: logs.reduce((a, b) => a + b.following.inc, 0),
+ dec: logs.reduce((a, b) => a + b.following.dec, 0),
+ },
+ followers: {
+ total: logs[0].followers.total,
+ inc: logs.reduce((a, b) => a + b.followers.inc, 0),
+ dec: logs.reduce((a, b) => a + b.followers.dec, 0),
+ },
+ drive: {
+ totalFiles: logs[0].drive.totalFiles,
+ totalUsage: logs[0].drive.totalUsage,
+ incFiles: logs.reduce((a, b) => a + b.drive.incFiles, 0),
+ incUsage: logs.reduce((a, b) => a + b.drive.incUsage, 0),
+ decFiles: logs.reduce((a, b) => a + b.drive.decFiles, 0),
+ decUsage: logs.reduce((a, b) => a + b.drive.decUsage, 0),
+ },
+ };
+ }
+
+ @autobind
protected async fetchActual(group: string): Promise<DeepPartial<InstanceLog>> {
const [
notesCount,
diff --git a/src/services/chart/charts/classes/network.ts b/src/services/chart/charts/classes/network.ts
index 8b26e5c4c2..693af48f73 100644
--- a/src/services/chart/charts/classes/network.ts
+++ b/src/services/chart/charts/classes/network.ts
@@ -16,6 +16,17 @@ export default class NetworkChart extends Chart<NetworkLog> {
}
@autobind
+ protected aggregate(logs: NetworkLog[]): NetworkLog {
+ return {
+ incomingRequests: logs.reduce((a, b) => a + b.incomingRequests, 0),
+ outgoingRequests: logs.reduce((a, b) => a + b.outgoingRequests, 0),
+ totalTime: logs.reduce((a, b) => a + b.totalTime, 0),
+ incomingBytes: logs.reduce((a, b) => a + b.incomingBytes, 0),
+ outgoingBytes: logs.reduce((a, b) => a + b.outgoingBytes, 0),
+ };
+ }
+
+ @autobind
protected async fetchActual(): Promise<DeepPartial<NetworkLog>> {
return {};
}
diff --git a/src/services/chart/charts/classes/notes.ts b/src/services/chart/charts/classes/notes.ts
index 815061c445..965087bc08 100644
--- a/src/services/chart/charts/classes/notes.ts
+++ b/src/services/chart/charts/classes/notes.ts
@@ -26,6 +26,32 @@ export default class NotesChart extends Chart<NotesLog> {
}
@autobind
+ protected aggregate(logs: NotesLog[]): NotesLog {
+ return {
+ local: {
+ total: logs[0].local.total,
+ inc: logs.reduce((a, b) => a + b.local.inc, 0),
+ dec: logs.reduce((a, b) => a + b.local.dec, 0),
+ diffs: {
+ reply: logs.reduce((a, b) => a + b.local.diffs.reply, 0),
+ renote: logs.reduce((a, b) => a + b.local.diffs.renote, 0),
+ normal: logs.reduce((a, b) => a + b.local.diffs.normal, 0),
+ },
+ },
+ remote: {
+ total: logs[0].remote.total,
+ inc: logs.reduce((a, b) => a + b.remote.inc, 0),
+ dec: logs.reduce((a, b) => a + b.remote.dec, 0),
+ diffs: {
+ reply: logs.reduce((a, b) => a + b.remote.diffs.reply, 0),
+ renote: logs.reduce((a, b) => a + b.remote.diffs.renote, 0),
+ normal: logs.reduce((a, b) => a + b.remote.diffs.normal, 0),
+ },
+ },
+ };
+ }
+
+ @autobind
protected async fetchActual(): Promise<DeepPartial<NotesLog>> {
const [localCount, remoteCount] = await Promise.all([
Notes.count({ userHost: null }),
diff --git a/src/services/chart/charts/classes/per-user-drive.ts b/src/services/chart/charts/classes/per-user-drive.ts
index aed9f6fce7..e778f7bf61 100644
--- a/src/services/chart/charts/classes/per-user-drive.ts
+++ b/src/services/chart/charts/classes/per-user-drive.ts
@@ -21,6 +21,18 @@ export default class PerUserDriveChart extends Chart<PerUserDriveLog> {
}
@autobind
+ protected aggregate(logs: PerUserDriveLog[]): PerUserDriveLog {
+ return {
+ totalCount: logs[0].totalCount,
+ totalSize: logs[0].totalSize,
+ incCount: logs.reduce((a, b) => a + b.incCount, 0),
+ incSize: logs.reduce((a, b) => a + b.incSize, 0),
+ decCount: logs.reduce((a, b) => a + b.decCount, 0),
+ decSize: logs.reduce((a, b) => a + b.decSize, 0),
+ };
+ }
+
+ @autobind
protected async fetchActual(group: string): Promise<DeepPartial<PerUserDriveLog>> {
const [count, size] = await Promise.all([
DriveFiles.count({ userId: group }),
diff --git a/src/services/chart/charts/classes/per-user-following.ts b/src/services/chart/charts/classes/per-user-following.ts
index 8295c0cb0d..8b536009c8 100644
--- a/src/services/chart/charts/classes/per-user-following.ts
+++ b/src/services/chart/charts/classes/per-user-following.ts
@@ -36,6 +36,36 @@ export default class PerUserFollowingChart extends Chart<PerUserFollowingLog> {
}
@autobind
+ protected aggregate(logs: PerUserFollowingLog[]): PerUserFollowingLog {
+ return {
+ local: {
+ followings: {
+ total: logs[0].local.followings.total,
+ inc: logs.reduce((a, b) => a + b.local.followings.inc, 0),
+ dec: logs.reduce((a, b) => a + b.local.followings.dec, 0),
+ },
+ followers: {
+ total: logs[0].local.followers.total,
+ inc: logs.reduce((a, b) => a + b.local.followers.inc, 0),
+ dec: logs.reduce((a, b) => a + b.local.followers.dec, 0),
+ },
+ },
+ remote: {
+ followings: {
+ total: logs[0].remote.followings.total,
+ inc: logs.reduce((a, b) => a + b.remote.followings.inc, 0),
+ dec: logs.reduce((a, b) => a + b.remote.followings.dec, 0),
+ },
+ followers: {
+ total: logs[0].remote.followers.total,
+ inc: logs.reduce((a, b) => a + b.remote.followers.inc, 0),
+ dec: logs.reduce((a, b) => a + b.remote.followers.dec, 0),
+ },
+ },
+ };
+ }
+
+ @autobind
protected async fetchActual(group: string): Promise<DeepPartial<PerUserFollowingLog>> {
const [
localFollowingsCount,
diff --git a/src/services/chart/charts/classes/per-user-notes.ts b/src/services/chart/charts/classes/per-user-notes.ts
index cccd495604..8d1fb8d2b0 100644
--- a/src/services/chart/charts/classes/per-user-notes.ts
+++ b/src/services/chart/charts/classes/per-user-notes.ts
@@ -21,6 +21,20 @@ export default class PerUserNotesChart extends Chart<PerUserNotesLog> {
}
@autobind
+ protected aggregate(logs: PerUserNotesLog[]): PerUserNotesLog {
+ return {
+ total: logs[0].total,
+ inc: logs.reduce((a, b) => a + b.inc, 0),
+ dec: logs.reduce((a, b) => a + b.dec, 0),
+ diffs: {
+ reply: logs.reduce((a, b) => a + b.diffs.reply, 0),
+ renote: logs.reduce((a, b) => a + b.diffs.renote, 0),
+ normal: logs.reduce((a, b) => a + b.diffs.normal, 0),
+ },
+ };
+ }
+
+ @autobind
protected async fetchActual(group: string): Promise<DeepPartial<PerUserNotesLog>> {
const [count] = await Promise.all([
Notes.count({ userId: group }),
diff --git a/src/services/chart/charts/classes/per-user-reactions.ts b/src/services/chart/charts/classes/per-user-reactions.ts
index 124fb4153c..b4cdced40c 100644
--- a/src/services/chart/charts/classes/per-user-reactions.ts
+++ b/src/services/chart/charts/classes/per-user-reactions.ts
@@ -19,6 +19,18 @@ export default class PerUserReactionsChart extends Chart<PerUserReactionsLog> {
}
@autobind
+ protected aggregate(logs: PerUserReactionsLog[]): PerUserReactionsLog {
+ return {
+ local: {
+ count: logs.reduce((a, b) => a + b.local.count, 0),
+ },
+ remote: {
+ count: logs.reduce((a, b) => a + b.remote.count, 0),
+ },
+ };
+ }
+
+ @autobind
protected async fetchActual(group: string): Promise<DeepPartial<PerUserReactionsLog>> {
return {};
}
diff --git a/src/services/chart/charts/classes/test-grouped.ts b/src/services/chart/charts/classes/test-grouped.ts
index e32cbcf416..92c8df636e 100644
--- a/src/services/chart/charts/classes/test-grouped.ts
+++ b/src/services/chart/charts/classes/test-grouped.ts
@@ -22,6 +22,17 @@ export default class TestGroupedChart extends Chart<TestGroupedLog> {
}
@autobind
+ protected aggregate(logs: TestGroupedLog[]): TestGroupedLog {
+ return {
+ foo: {
+ total: logs[0].foo.total,
+ inc: logs.reduce((a, b) => a + b.foo.inc, 0),
+ dec: logs.reduce((a, b) => a + b.foo.dec, 0),
+ },
+ };
+ }
+
+ @autobind
protected async fetchActual(group: string): Promise<DeepPartial<TestGroupedLog>> {
return {
foo: {
diff --git a/src/services/chart/charts/classes/test-unique.ts b/src/services/chart/charts/classes/test-unique.ts
index 1eb396c293..5680d713ec 100644
--- a/src/services/chart/charts/classes/test-unique.ts
+++ b/src/services/chart/charts/classes/test-unique.ts
@@ -16,14 +16,21 @@ export default class TestUniqueChart extends Chart<TestUniqueLog> {
}
@autobind
+ protected aggregate(logs: TestUniqueLog[]): TestUniqueLog {
+ return {
+ foo: logs.reduce((a, b) => a.concat(b.foo), [] as TestUniqueLog['foo']),
+ };
+ }
+
+ @autobind
protected async fetchActual(): Promise<DeepPartial<TestUniqueLog>> {
return {};
}
@autobind
public async uniqueIncrement(key: string) {
- await this.incIfUnique({
- foo: 1
- }, 'foos', key);
+ await this.inc({
+ foo: [key]
+ });
}
}
diff --git a/src/services/chart/charts/classes/test.ts b/src/services/chart/charts/classes/test.ts
index ea64040f3e..d37d298de7 100644
--- a/src/services/chart/charts/classes/test.ts
+++ b/src/services/chart/charts/classes/test.ts
@@ -22,6 +22,17 @@ export default class TestChart extends Chart<TestLog> {
}
@autobind
+ protected aggregate(logs: TestLog[]): TestLog {
+ return {
+ foo: {
+ total: logs[0].foo.total,
+ inc: logs.reduce((a, b) => a + b.foo.inc, 0),
+ dec: logs.reduce((a, b) => a + b.foo.dec, 0),
+ },
+ };
+ }
+
+ @autobind
protected async fetchActual(): Promise<DeepPartial<TestLog>> {
return {
foo: {
diff --git a/src/services/chart/charts/classes/users.ts b/src/services/chart/charts/classes/users.ts
index 47e4caa1b7..87b19d88f9 100644
--- a/src/services/chart/charts/classes/users.ts
+++ b/src/services/chart/charts/classes/users.ts
@@ -26,6 +26,22 @@ export default class UsersChart extends Chart<UsersLog> {
}
@autobind
+ protected aggregate(logs: UsersLog[]): UsersLog {
+ return {
+ local: {
+ total: logs[0].local.total,
+ inc: logs.reduce((a, b) => a + b.local.inc, 0),
+ dec: logs.reduce((a, b) => a + b.local.dec, 0),
+ },
+ remote: {
+ total: logs[0].remote.total,
+ inc: logs.reduce((a, b) => a + b.remote.inc, 0),
+ dec: logs.reduce((a, b) => a + b.remote.dec, 0),
+ },
+ };
+ }
+
+ @autobind
protected async fetchActual(): Promise<DeepPartial<UsersLog>> {
const [localCount, remoteCount] = await Promise.all([
Users.count({ host: null }),
diff --git a/src/services/chart/charts/schemas/active-users.ts b/src/services/chart/charts/schemas/active-users.ts
index 6e26bb4698..cdf0579efb 100644
--- a/src/services/chart/charts/schemas/active-users.ts
+++ b/src/services/chart/charts/schemas/active-users.ts
@@ -1,11 +1,15 @@
export const logSchema = {
/**
- * アクティブユーザー数
+ * アクティブユーザー
*/
- count: {
- type: 'number' as const,
+ users: {
+ type: 'array' as const,
optional: false as const, nullable: false as const,
- description: 'アクティブユーザー数',
+ description: 'アクティブユーザー',
+ items: {
+ type: 'string' as const,
+ optional: false as const, nullable: false as const,
+ }
},
};
diff --git a/src/services/chart/charts/schemas/hashtag.ts b/src/services/chart/charts/schemas/hashtag.ts
index 4dfd61c97f..791d0d1721 100644
--- a/src/services/chart/charts/schemas/hashtag.ts
+++ b/src/services/chart/charts/schemas/hashtag.ts
@@ -1,11 +1,15 @@
export const logSchema = {
/**
- * 投稿された数
+ * 投稿したユーザー
*/
- count: {
- type: 'number' as const,
+ users: {
+ type: 'array' as const,
optional: false as const, nullable: false as const,
- description: '投稿された数',
+ description: '投稿したユーザー',
+ items: {
+ type: 'string' as const,
+ optional: false as const, nullable: false as const,
+ }
},
};
diff --git a/src/services/chart/charts/schemas/test-unique.ts b/src/services/chart/charts/schemas/test-unique.ts
index 075a8092d9..51280400ac 100644
--- a/src/services/chart/charts/schemas/test-unique.ts
+++ b/src/services/chart/charts/schemas/test-unique.ts
@@ -3,9 +3,12 @@ export const schema = {
optional: false as const, nullable: false as const,
properties: {
foo: {
- type: 'number' as const,
+ type: 'array' as const,
optional: false as const, nullable: false as const,
- description: ''
+ items: {
+ type: 'string' as const,
+ optional: false as const, nullable: false as const,
+ }
},
}
};
diff --git a/src/services/chart/core.ts b/src/services/chart/core.ts
index dc09923ae4..10621be073 100644
--- a/src/services/chart/core.ts
+++ b/src/services/chart/core.ts
@@ -24,8 +24,6 @@ type ArrayValue<T> = {
[P in keyof T]: T[P] extends number ? T[P][] : ArrayValue<T[P]>;
};
-type Span = 'day' | 'hour';
-
type Log = {
id: number;
@@ -38,22 +36,14 @@ type Log = {
* 集計日時のUnixタイムスタンプ(秒)
*/
date: number;
-
- /**
- * 集計期間
- */
- span: Span;
-
- /**
- * ユニークインクリメント用
- */
- unique?: Record<string, any>;
};
const camelToSnake = (str: string) => {
return str.replace(/([A-Z])/g, s => '_' + s.charAt(0).toLowerCase());
};
+const removeDuplicates = (array: any[]) => Array.from(new Set(array));
+
/**
* 様々なチャートの管理を司るクラス
*/
@@ -62,10 +52,21 @@ export default abstract class Chart<T extends Record<string, any>> {
private static readonly columnDot = '_';
private name: string;
+ private queue: {
+ diff: DeepPartial<T>;
+ group: string | null;
+ }[] = [];
public schema: Schema;
protected repository: Repository<Log>;
+
protected abstract genNewLog(latest: T): DeepPartial<T>;
- protected abstract async fetchActual(group: string | null): Promise<DeepPartial<T>>;
+
+ /**
+ * @param logs 日時が新しい方が先頭
+ */
+ protected abstract aggregate(logs: T[]): T;
+
+ protected abstract fetchActual(group: string | null): Promise<DeepPartial<T>>;
@autobind
private static convertSchemaToFlatColumnDefinitions(schema: Schema) {
@@ -75,10 +76,15 @@ export default abstract class Chart<T extends Record<string, any>> {
const p = path ? `${path}${this.columnDot}${k}` : k;
if (v.type === 'object') {
flatColumns(v.properties, p);
- } else {
+ } else if (v.type === 'number') {
columns[this.columnPrefix + p] = {
type: 'bigint',
};
+ } else if (v.type === 'array' && v.items.type === 'string') {
+ columns[this.columnPrefix + p] = {
+ type: 'varchar',
+ array: true,
+ };
}
}
};
@@ -99,11 +105,11 @@ export default abstract class Chart<T extends Record<string, any>> {
@autobind
private static convertObjectToFlattenColumns(x: Record<string, any>) {
- const columns = {} as Record<string, number>;
+ const columns = {} as Record<string, number | unknown[]>;
const flatten = (x: Obj, path?: string) => {
for (const [k, v] of Object.entries(x)) {
const p = path ? `${path}${this.columnDot}${k}` : k;
- if (typeof v === 'object') {
+ if (typeof v === 'object' && !Array.isArray(v)) {
flatten(v, p);
} else {
columns[this.columnPrefix + p] = v;
@@ -115,14 +121,37 @@ export default abstract class Chart<T extends Record<string, any>> {
}
@autobind
- private static convertQuery(x: Record<string, any>) {
- const query: Record<string, Function> = {};
+ private static countUniqueFields(x: Record<string, any>) {
+ const exec = (x: Obj) => {
+ const res = {} as Record<string, any>;
+ for (const [k, v] of Object.entries(x)) {
+ if (typeof v === 'object' && !Array.isArray(v)) {
+ res[k] = exec(v);
+ } else if (Array.isArray(v)) {
+ res[k] = Array.from(new Set(v)).length;
+ } else {
+ res[k] = v;
+ }
+ }
+ return res;
+ };
+ return exec(x);
+ }
- const columns = Chart.convertObjectToFlattenColumns(x);
+ @autobind
+ private static convertQuery(diff: Record<string, number | unknown[]>) {
+ const query: Record<string, Function> = {};
- for (const [k, v] of Object.entries(columns)) {
- if (v > 0) query[k] = () => `"${k}" + ${v}`;
- if (v < 0) query[k] = () => `"${k}" - ${Math.abs(v)}`;
+ for (const [k, v] of Object.entries(diff)) {
+ if (typeof v === 'number') {
+ if (v > 0) query[k] = () => `"${k}" + ${v}`;
+ if (v < 0) query[k] = () => `"${k}" - ${Math.abs(v)}`;
+ } else if (Array.isArray(v)) {
+ // TODO: item が文字列以外の場合も対応
+ // TODO: item をSQLエスケープ
+ const items = v.map(item => `"${item}"`).join(',');
+ query[k] = () => `array_cat("${k}", '{${items}}'::varchar[])`;
+ }
}
return query;
@@ -169,28 +198,14 @@ export default abstract class Chart<T extends Record<string, any>> {
length: 128,
nullable: true
},
- span: {
- type: 'enum',
- enum: ['hour', 'day']
- },
- unique: {
- type: 'jsonb',
- default: {}
- },
...Chart.convertSchemaToFlatColumnDefinitions(schema)
},
indices: [{
columns: ['date']
}, {
- columns: ['span']
- }, {
columns: ['group']
}, {
- columns: ['span', 'date']
- }, {
columns: ['date', 'group']
- }, {
- columns: ['span', 'date', 'group']
}]
});
}
@@ -200,7 +215,7 @@ export default abstract class Chart<T extends Record<string, any>> {
this.schema = schema;
const entity = Chart.schemaToEntity(name, schema);
- const keys = ['span', 'date'];
+ const keys = ['date'];
if (grouped) keys.push('group');
entity.options.uniques = [{
@@ -220,7 +235,8 @@ export default abstract class Chart<T extends Record<string, any>> {
flatColumns(v.properties, p);
} else {
if (nestedProperty.get(log, p) == null) {
- nestedProperty.set(log, p, 0);
+ const emptyValue = v.type === 'number' ? 0 : [];
+ nestedProperty.set(log, p, emptyValue);
}
}
}
@@ -230,10 +246,9 @@ export default abstract class Chart<T extends Record<string, any>> {
}
@autobind
- private getLatestLog(span: Span, group: string | null = null): Promise<Log | null> {
+ private getLatestLog(group: string | null = null): Promise<Log | null> {
return this.repository.findOne({
group: group,
- span: span
}, {
order: {
date: -1
@@ -242,17 +257,13 @@ export default abstract class Chart<T extends Record<string, any>> {
}
@autobind
- private async getCurrentLog(span: Span, group: string | null = null): Promise<Log> {
+ private async getCurrentLog(group: string | null = null): Promise<Log> {
const [y, m, d, h] = Chart.getCurrentDate();
- const current =
- span == 'day' ? dateUTC([y, m, d, 0]) :
- span == 'hour' ? dateUTC([y, m, d, h]) :
- null as never;
+ const current = dateUTC([y, m, d, h]);
- // 現在(今日または今のHour)のログ
+ // 現在(=今のHour)のログ
const currentLog = await this.repository.findOne({
- span: span,
date: Chart.dateToTimestamp(current),
...(group ? { group: group } : {})
});
@@ -271,7 +282,7 @@ export default abstract class Chart<T extends Record<string, any>> {
// * 昨日何もチャートを更新するような出来事がなかった場合は、
// * ログがそもそも作られずドキュメントが存在しないということがあり得るため、
// * 「昨日の」と決め打ちせずに「もっとも最近の」とします
- const latest = await this.getLatestLog(span, group);
+ const latest = await this.getLatestLog(group);
if (latest != null) {
const obj = Chart.convertFlattenColumnsToObject(
@@ -286,17 +297,16 @@ export default abstract class Chart<T extends Record<string, any>> {
// 初期ログデータを作成
data = this.getNewLog(null);
- logger.info(`${this.name + (group ? `:${group}` : '')} (${span}): Initial commit created`);
+ logger.info(`${this.name + (group ? `:${group}` : '')}: Initial commit created`);
}
const date = Chart.dateToTimestamp(current);
- const lockKey = `${this.name}:${date}:${group}:${span}`;
+ const lockKey = `${this.name}:${date}:${group}`;
const unlock = await getChartInsertLock(lockKey);
try {
// ロック内でもう1回チェックする
const currentLog = await this.repository.findOne({
- span: span,
date: date,
...(group ? { group: group } : {})
});
@@ -307,12 +317,11 @@ export default abstract class Chart<T extends Record<string, any>> {
// 新規ログ挿入
log = await this.repository.save({
group: group,
- span: span,
date: date,
...Chart.convertObjectToFlattenColumns(data)
});
- logger.info(`${this.name + (group ? `:${group}` : '')} (${span}): New commit created`);
+ logger.info(`${this.name + (group ? `:${group}` : '')}: New commit created`);
return log;
} finally {
@@ -321,38 +330,62 @@ export default abstract class Chart<T extends Record<string, any>> {
}
@autobind
- protected commit(query: Record<string, Function>, group: string | null = null, uniqueKey?: string, uniqueValue?: string): Promise<any> {
+ protected commit(diff: DeepPartial<T>, group: string | null = null): void {
+ this.queue.push({
+ diff, group,
+ });
+ }
+
+ @autobind
+ public async save() {
+ if (this.queue.length === 0) {
+ logger.info(`${this.name}: Write skipped`);
+ return;
+ }
+
+ // TODO: 前の時間のログがqueueにあった場合のハンドリング
+ // 例えば、save が20分ごとに行われるとして、前回行われたのは 01:50 だったとする。
+ // 次に save が行われるのは 02:10 ということになるが、もし 01:55 に新規ログが queue に追加されたとすると、
+ // そのログは本来は 01:00~ のログとしてDBに保存されて欲しいのに、02:00~ のログ扱いになってしまう。
+ // これを回避するための実装は複雑になりそうなため、一旦保留。
+
const update = async (log: Log) => {
- // ユニークインクリメントの場合、指定のキーに指定の値が既に存在していたら弾く
- if (
- uniqueKey && log.unique &&
- log.unique[uniqueKey] &&
- log.unique[uniqueKey].includes(uniqueValue)
- ) return;
+ const finalDiffs = {} as Record<string, number | unknown[]>;
- // ユニークインクリメントの指定のキーに値を追加
- if (uniqueKey && log.unique) {
- if (log.unique[uniqueKey]) {
- const sql = `jsonb_set("unique", '{${uniqueKey}}', ("unique"->>'${uniqueKey}')::jsonb || '["${uniqueValue}"]'::jsonb)`;
- query['unique'] = () => sql;
- } else {
- const sql = `jsonb_set("unique", '{${uniqueKey}}', '["${uniqueValue}"]')`;
- query['unique'] = () => sql;
+ for (const diff of this.queue.filter(q => q.group === log.group).map(q => q.diff)) {
+ const columns = Chart.convertObjectToFlattenColumns(diff);
+
+ for (const [k, v] of Object.entries(columns)) {
+ if (finalDiffs[k] == null) {
+ finalDiffs[k] = v;
+ } else {
+ if (typeof finalDiffs[k] === 'number') {
+ (finalDiffs[k] as number) += v as number;
+ } else {
+ (finalDiffs[k] as unknown[]) = (finalDiffs[k] as unknown[]).concat(v);
+ }
+ }
}
}
+ const query = Chart.convertQuery(finalDiffs);
+
// ログ更新
await this.repository.createQueryBuilder()
.update()
.set(query)
.where('id = :id', { id: log.id })
.execute();
+
+ logger.info(`${this.name + (log.group ? `:${log.group}` : '')}: Updated`);
+
+ // TODO: この一連の処理が始まった後に新たにqueueに入ったものは消さないようにする
+ this.queue = this.queue.filter(q => q.group !== log.group);
};
- return Promise.all([
- this.getCurrentLog('day', group).then(log => update(log)),
- this.getCurrentLog('hour', group).then(log => update(log)),
- ]);
+ const groups = removeDuplicates(this.queue.map(log => log.group));
+
+ await Promise.all(groups.map(group => this.getCurrentLog(group).then(log => update(log))));
}
@autobind
@@ -367,39 +400,30 @@ export default abstract class Chart<T extends Record<string, any>> {
.execute();
};
- return Promise.all([
- this.getCurrentLog('day', group).then(log => update(log)),
- this.getCurrentLog('hour', group).then(log => update(log)),
- ]);
+ return this.getCurrentLog(group).then(log => update(log));
}
@autobind
protected async inc(inc: DeepPartial<T>, group: string | null = null): Promise<void> {
- await this.commit(Chart.convertQuery(inc as any), group);
- }
-
- @autobind
- protected async incIfUnique(inc: DeepPartial<T>, key: string, value: string, group: string | null = null): Promise<void> {
- await this.commit(Chart.convertQuery(inc as any), group, key, value);
+ await this.commit(inc, group);
}
@autobind
- public async getChart(span: Span, amount: number, begin: Date | null, group: string | null = null): Promise<ArrayValue<T>> {
- const [y, m, d, h, _m, _s, _ms] = begin ? Chart.parseDate(subtractTime(addTime(begin, 1, span), 1)) : Chart.getCurrentDate();
- const [y2, m2, d2, h2] = begin ? Chart.parseDate(addTime(begin, 1, span)) : [] as never;
+ public async getChart(span: 'hour' | 'day', amount: number, cursor: Date | null, group: string | null = null): Promise<ArrayValue<T>> {
+ const [y, m, d, h, _m, _s, _ms] = cursor ? Chart.parseDate(subtractTime(addTime(cursor, 1, span), 1)) : Chart.getCurrentDate();
+ const [y2, m2, d2, h2] = cursor ? Chart.parseDate(addTime(cursor, 1, span)) : [] as never;
const lt = dateUTC([y, m, d, h, _m, _s, _ms]);
const gt =
- span === 'day' ? subtractTime(begin ? dateUTC([y2, m2, d2, 0]) : dateUTC([y, m, d, 0]), amount - 1, 'day') :
- span === 'hour' ? subtractTime(begin ? dateUTC([y2, m2, d2, h2]) : dateUTC([y, m, d, h]), amount - 1, 'hour') :
+ span === 'day' ? subtractTime(cursor ? dateUTC([y2, m2, d2, 0]) : dateUTC([y, m, d, 0]), amount - 1, 'day') :
+ span === 'hour' ? subtractTime(cursor ? dateUTC([y2, m2, d2, h2]) : dateUTC([y, m, d, h]), amount - 1, 'hour') :
null as never;
// ログ取得
let logs = await this.repository.find({
where: {
group: group,
- span: span,
date: Between(Chart.dateToTimestamp(gt), Chart.dateToTimestamp(lt))
},
order: {
@@ -413,7 +437,6 @@ export default abstract class Chart<T extends Record<string, any>> {
// (すくなくともひとつログが無いと隙間埋めできないため)
const recentLog = await this.repository.findOne({
group: group,
- span: span
}, {
order: {
date: -1
@@ -430,7 +453,6 @@ export default abstract class Chart<T extends Record<string, any>> {
// (隙間埋めできないため)
const outdatedLog = await this.repository.findOne({
group: group,
- span: span,
date: LessThan(Chart.dateToTimestamp(gt))
}, {
order: {
@@ -445,23 +467,56 @@ export default abstract class Chart<T extends Record<string, any>> {
const chart: T[] = [];
- // 整形
- for (let i = (amount - 1); i >= 0; i--) {
- const current =
- span === 'day' ? subtractTime(dateUTC([y, m, d, 0]), i, 'day') :
- span === 'hour' ? subtractTime(dateUTC([y, m, d, h]), i, 'hour') :
- null as never;
+ if (span === 'hour') {
+ for (let i = (amount - 1); i >= 0; i--) {
+ const current = subtractTime(dateUTC([y, m, d, h]), i, 'hour');
- const log = logs.find(l => isTimeSame(new Date(l.date * 1000), current));
+ const log = logs.find(l => isTimeSame(new Date(l.date * 1000), current));
+
+ if (log) {
+ const data = Chart.convertFlattenColumnsToObject(log as Record<string, any>);
+ chart.unshift(Chart.countUniqueFields(data));
+ } else {
+ // 隙間埋め
+ const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current));
+ const data = latest ? Chart.convertFlattenColumnsToObject(latest as Record<string, any>) : null;
+ chart.unshift(Chart.countUniqueFields(this.getNewLog(data)));
+ }
+ }
+ } else if (span === 'day') {
+ const logsForEachDays: T[][] = [];
+ let currentDay = -1;
+ let currentDayIndex = -1;
+ for (let i = ((amount - 1) * 24) + h; i >= 0; i--) {
+ const current = subtractTime(dateUTC([y, m, d, h]), i, 'hour');
+ const _currentDay = Chart.parseDate(current)[2];
+ if (currentDay != _currentDay) currentDayIndex++;
+ currentDay = _currentDay;
+
+ const log = logs.find(l => isTimeSame(new Date(l.date * 1000), current));
+
+ if (log) {
+ if (logsForEachDays[currentDayIndex]) {
+ logsForEachDays[currentDayIndex].unshift(Chart.convertFlattenColumnsToObject(log));
+ } else {
+ logsForEachDays[currentDayIndex] = [Chart.convertFlattenColumnsToObject(log)];
+ }
+ } else {
+ // 隙間埋め
+ const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current));
+ const data = latest ? Chart.convertFlattenColumnsToObject(latest as Record<string, any>) : null;
+ const newLog = this.getNewLog(data);
+ if (logsForEachDays[currentDayIndex]) {
+ logsForEachDays[currentDayIndex].unshift(newLog);
+ } else {
+ logsForEachDays[currentDayIndex] = [newLog];
+ }
+ }
+ }
- if (log) {
- const data = Chart.convertFlattenColumnsToObject(log as Record<string, any>);
- chart.unshift(data);
- } else {
- // 隙間埋め
- const latest = logs.find(l => isTimeBefore(new Date(l.date * 1000), current));
- const data = latest ? Chart.convertFlattenColumnsToObject(latest as Record<string, any>) : null;
- chart.unshift(this.getNewLog(data));
+ for (const logs of logsForEachDays) {
+ const log = this.aggregate(logs);
+ chart.unshift(Chart.countUniqueFields(log));
}
}
@@ -473,20 +528,19 @@ export default abstract class Chart<T extends Record<string, any>> {
* { foo: [1, 2, 3], bar: [5, 6, 7] }
* にする
*/
- const dive = (x: Obj, path?: string) => {
+ const compact = (x: Obj, path?: string) => {
for (const [k, v] of Object.entries(x)) {
const p = path ? `${path}.${k}` : k;
- if (typeof v == 'object') {
- dive(v, p);
+ if (typeof v === 'object' && !Array.isArray(v)) {
+ compact(v, p);
} else {
- const values = chart.map(s => nestedProperty.get(s, p))
- .map(v => parseInt(v, 10)); // TypeORMのバグ(?)で何故か数値カラムの値が文字列型になっているので数値に戻す
+ const values = chart.map(s => nestedProperty.get(s, p));
nestedProperty.set(res, p, values);
}
}
};
- dive(chart[0]);
+ compact(chart[0]);
return res;
}
diff --git a/src/services/chart/index.ts b/src/services/chart/index.ts
index 9626e3d6b3..dde02bd64d 100644
--- a/src/services/chart/index.ts
+++ b/src/services/chart/index.ts
@@ -10,6 +10,7 @@ import PerUserReactionsChart from './charts/classes/per-user-reactions';
import HashtagChart from './charts/classes/hashtag';
import PerUserFollowingChart from './charts/classes/per-user-following';
import PerUserDriveChart from './charts/classes/per-user-drive';
+import { beforeShutdown } from '../../misc/before-shutdown';
export const federationChart = new FederationChart();
export const notesChart = new NotesChart();
@@ -23,3 +24,27 @@ export const perUserReactionsChart = new PerUserReactionsChart();
export const hashtagChart = new HashtagChart();
export const perUserFollowingChart = new PerUserFollowingChart();
export const perUserDriveChart = new PerUserDriveChart();
+
+const charts = [
+ federationChart,
+ notesChart,
+ usersChart,
+ networkChart,
+ activeUsersChart,
+ instanceChart,
+ perUserNotesChart,
+ driveChart,
+ perUserReactionsChart,
+ hashtagChart,
+ perUserFollowingChart,
+ perUserDriveChart,
+];
+
+// 20分おきにメモリ情報をDBに書き込み
+setInterval(() => {
+ for (const chart of charts) {
+ chart.save();
+ }
+}, 1000 * 60 * 20);
+
+beforeShutdown(() => Promise.all(charts.map(chart => chart.save())));
diff --git a/src/services/create-notification.ts b/src/services/create-notification.ts
index 6cd116040a..dedb8eac8d 100644
--- a/src/services/create-notification.ts
+++ b/src/services/create-notification.ts
@@ -30,7 +30,7 @@ export async function createNotification(
...data
} as Partial<Notification>);
- const packed = await Notifications.pack(notification);
+ const packed = await Notifications.pack(notification, {});
// Publish notification event
publishMainStream(notifieeId, 'notification', packed);
diff --git a/src/services/following/create.ts b/src/services/following/create.ts
index c0583cdb86..1ce75caca0 100644
--- a/src/services/following/create.ts
+++ b/src/services/following/create.ts
@@ -1,4 +1,4 @@
-import { publishMainStream } from '../stream';
+import { publishMainStream, publishUserEvent } from '../stream';
import { renderActivity } from '../../remote/activitypub/renderer';
import renderFollow from '../../remote/activitypub/renderer/follow';
import renderAccept from '../../remote/activitypub/renderer/accept';
@@ -22,7 +22,7 @@ export async function insertFollowingDoc(followee: User, follower: User) {
let alreadyFollowed = false;
- await Followings.save({
+ await Followings.insert({
id: genId(),
createdAt: new Date(),
followerId: follower.id,
@@ -88,12 +88,15 @@ export async function insertFollowingDoc(followee: User, follower: User) {
if (Users.isLocalUser(follower)) {
Users.pack(followee, follower, {
detail: true
- }).then(packed => publishMainStream(follower.id, 'follow', packed));
+ }).then(packed => {
+ publishUserEvent(follower.id, 'follow', packed);
+ publishMainStream(follower.id, 'follow', packed);
+ });
}
// Publish followed event
if (Users.isLocalUser(followee)) {
- Users.pack(follower, followee).then(packed => publishMainStream(followee.id, 'followed', packed)),
+ Users.pack(follower, followee).then(packed => publishMainStream(followee.id, 'followed', packed));
// 通知を作成
createNotification(followee.id, 'follow', {
diff --git a/src/services/following/delete.ts b/src/services/following/delete.ts
index 8821611515..32c47ea7f4 100644
--- a/src/services/following/delete.ts
+++ b/src/services/following/delete.ts
@@ -1,4 +1,4 @@
-import { publishMainStream } from '../stream';
+import { publishMainStream, publishUserEvent } from '../stream';
import { renderActivity } from '../../remote/activitypub/renderer';
import renderFollow from '../../remote/activitypub/renderer/follow';
import renderUndo from '../../remote/activitypub/renderer/undo';
@@ -30,7 +30,10 @@ export default async function(follower: User, followee: User, silent = false) {
if (!silent && Users.isLocalUser(follower)) {
Users.pack(followee, follower, {
detail: true
- }).then(packed => publishMainStream(follower.id, 'unfollow', packed));
+ }).then(packed => {
+ publishUserEvent(follower.id, 'unfollow', packed);
+ publishMainStream(follower.id, 'unfollow', packed);
+ });
}
if (Users.isLocalUser(follower) && Users.isRemoteUser(followee)) {
diff --git a/src/services/following/requests/reject.ts b/src/services/following/requests/reject.ts
index 9a8b14bbfd..d8d3788088 100644
--- a/src/services/following/requests/reject.ts
+++ b/src/services/following/requests/reject.ts
@@ -2,7 +2,7 @@ import { renderActivity } from '../../../remote/activitypub/renderer';
import renderFollow from '../../../remote/activitypub/renderer/follow';
import renderReject from '../../../remote/activitypub/renderer/reject';
import { deliver } from '../../../queue';
-import { publishMainStream } from '../../stream';
+import { publishMainStream, publishUserEvent } from '../../stream';
import { User, ILocalUser } from '../../../models/entities/user';
import { Users, FollowRequests, Followings } from '../../../models';
import { decrementFollowing } from '../delete';
@@ -39,5 +39,8 @@ export default async function(followee: User, follower: User) {
Users.pack(followee, follower, {
detail: true
- }).then(packed => publishMainStream(follower.id, 'unfollow', packed));
+ }).then(packed => {
+ publishUserEvent(follower.id, 'unfollow', packed);
+ publishMainStream(follower.id, 'unfollow', packed);
+ });
}
diff --git a/src/services/i/pin.ts b/src/services/i/pin.ts
index 1ff5476b40..f727a10fb6 100644
--- a/src/services/i/pin.ts
+++ b/src/services/i/pin.ts
@@ -37,7 +37,7 @@ export async function addPinned(user: User, noteId: Note['id']) {
throw new IdentifiableError('23f0cf4e-59a3-4276-a91d-61a5891c1514', 'That note has already been pinned.');
}
- await UserNotePinings.save({
+ await UserNotePinings.insert({
id: genId(),
createdAt: new Date(),
userId: user.id,
diff --git a/src/services/insert-moderation-log.ts b/src/services/insert-moderation-log.ts
index 33dab97259..87587d3bed 100644
--- a/src/services/insert-moderation-log.ts
+++ b/src/services/insert-moderation-log.ts
@@ -3,7 +3,7 @@ import { ModerationLogs } from '../models';
import { genId } from '../misc/gen-id';
export async function insertModerationLog(moderator: ILocalUser, type: string, info?: Record<string, any>) {
- await ModerationLogs.save({
+ await ModerationLogs.insert({
id: genId(),
createdAt: new Date(),
userId: moderator.id,
diff --git a/src/services/messages/create.ts b/src/services/messages/create.ts
index 8646ce37fc..413266d029 100644
--- a/src/services/messages/create.ts
+++ b/src/services/messages/create.ts
@@ -14,7 +14,7 @@ import { renderActivity } from '../../remote/activitypub/renderer';
import { deliver } from '../../queue';
export async function createMessage(user: User, recipientUser: User | undefined, recipientGroup: UserGroup | undefined, text: string | undefined, file: DriveFile | null, uri?: string) {
- const message = await MessagingMessages.save({
+ const message = {
id: genId(),
createdAt: new Date(),
fileId: file ? file.id : null,
@@ -25,7 +25,9 @@ export async function createMessage(user: User, recipientUser: User | undefined,
isRead: false,
reads: [] as any[],
uri
- } as MessagingMessage);
+ } as MessagingMessage;
+
+ await MessagingMessages.insert(message);
const messageObj = await MessagingMessages.pack(message);
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index 563eaac758..4a737e8516 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -247,7 +247,7 @@ export default async (user: User, data: Option, silent = false) => new Promise<N
for (const u of us) {
checkWordMute(note, { id: u.userId }, u.mutedWords).then(shouldMute => {
if (shouldMute) {
- MutedNotes.save({
+ MutedNotes.insert({
id: genId(),
userId: u.userId,
noteId: note.id,
@@ -259,21 +259,21 @@ export default async (user: User, data: Option, silent = false) => new Promise<N
});
// Antenna
- Antennas.find().then(async antennas => {
- const followings = await Followings.createQueryBuilder('following')
- .andWhere(`following.followeeId = :userId`, { userId: note.userId })
- .getMany();
-
- const followers = followings.map(f => f.followerId);
-
- for (const antenna of antennas) {
- checkHitAntenna(antenna, note, user, followers).then(hit => {
- if (hit) {
- addNoteToAntenna(antenna, note, user);
+ Followings.createQueryBuilder('following')
+ .andWhere(`following.followeeId = :userId`, { userId: note.userId })
+ .getMany()
+ .then(followings => {
+ const followers = followings.map(f => f.followerId);
+ Antennas.find().then(async antennas => {
+ for (const antenna of antennas) {
+ checkHitAntenna(antenna, note, user, followers).then(hit => {
+ if (hit) {
+ addNoteToAntenna(antenna, note, user);
+ }
+ });
}
});
- }
- });
+ });
// Channel
if (note.channelId) {
@@ -444,8 +444,13 @@ async function renderNoteOrRenoteActivity(data: Option, note: Note) {
}
function incRenoteCount(renote: Note) {
- Notes.increment({ id: renote.id }, 'renoteCount', 1);
- Notes.increment({ id: renote.id }, 'score', 1);
+ Notes.createQueryBuilder().update()
+ .set({
+ renoteCount: () => '"renoteCount" + 1',
+ score: () => '"score" + 1'
+ })
+ .where('id = :id', { id: renote.id })
+ .execute();
}
async function insertNote(user: User, data: Option, tags: string[], emojis: string[], mentionedUsers: User[]) {
@@ -525,7 +530,7 @@ async function insertNote(user: User, data: Option, tags: string[], emojis: stri
await Notes.insert(insert);
}
- return await Notes.findOneOrFail(insert.id);
+ return insert;
} catch (e) {
// duplicate key error
if (isDuplicateKeyValueError(e)) {
@@ -594,10 +599,13 @@ function saveReply(reply: Note, note: Note) {
}
function incNotesCountOfUser(user: User) {
- Users.increment({ id: user.id }, 'notesCount', 1);
- Users.update({ id: user.id }, {
- updatedAt: new Date()
- });
+ Users.createQueryBuilder().update()
+ .set({
+ updatedAt: new Date(),
+ notesCount: () => '"notesCount" + 1'
+ })
+ .where('id = :id', { id: user.id })
+ .execute();
}
async function extractMentionedUsers(user: User, tokens: ReturnType<typeof parse>): Promise<User[]> {
diff --git a/src/services/note/polls/vote.ts b/src/services/note/polls/vote.ts
index bfcaaa09be..b4ce03ab60 100644
--- a/src/services/note/polls/vote.ts
+++ b/src/services/note/polls/vote.ts
@@ -29,7 +29,7 @@ export default async function(user: User, note: Note, choice: number) {
}
// Create vote
- await PollVotes.save({
+ await PollVotes.insert({
id: genId(),
createdAt: new Date(),
noteId: note.id,
diff --git a/src/services/note/reaction/create.ts b/src/services/note/reaction/create.ts
index adc96ddc1f..181099cc2d 100644
--- a/src/services/note/reaction/create.ts
+++ b/src/services/note/reaction/create.ts
@@ -11,45 +11,53 @@ import { perUserReactionsChart } from '../../chart';
import { genId } from '../../../misc/gen-id';
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';
export default async (user: User, note: Note, reaction?: string) => {
+ // TODO: cache
reaction = await toDbReaction(reaction, user.host);
- const exist = await NoteReactions.findOne({
+ let record: NoteReaction = {
+ id: genId(),
+ createdAt: new Date(),
noteId: note.id,
userId: user.id,
- });
+ reaction
+ };
- if (exist) {
- if (exist.reaction !== reaction) {
- // 別のリアクションがすでにされていたら置き換える
- await deleteReaction(user, note);
+ // Create reaction
+ try {
+ await NoteReactions.insert(record);
+ } catch (e) {
+ if (isDuplicateKeyValueError(e)) {
+ record = await NoteReactions.findOneOrFail({
+ noteId: note.id,
+ userId: user.id,
+ });
+
+ if (record.reaction !== reaction) {
+ // 別のリアクションがすでにされていたら置き換える
+ await deleteReaction(user, note);
+ } else {
+ // 同じリアクションがすでにされていたら何もしない
+ return;
+ }
} else {
- // 同じリアクションがすでにされていたら何もしない
- return;
+ throw e;
}
}
- // Create reaction
- const inserted = await NoteReactions.save({
- id: genId(),
- createdAt: new Date(),
- noteId: note.id,
- userId: user.id,
- reaction
- });
-
// Increment reactions count
const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
await Notes.createQueryBuilder().update()
.set({
reactions: () => sql,
+ score: () => '"score" + 1'
})
.where('id = :id', { id: note.id })
.execute();
- Notes.increment({ id: note.id }, 'score', 1);
-
perUserReactionsChart.update(user, note);
// カスタム絵文字リアクションだったら絵文字情報も送る
@@ -101,7 +109,7 @@ export default async (user: User, note: Note, reaction?: string) => {
//#region 配信
if (Users.isLocalUser(user) && !note.localOnly) {
- const content = renderActivity(await renderLike(inserted, note));
+ const content = renderActivity(await renderLike(record, note));
const dm = new DeliverManager(user, content);
if (note.userHost !== null) {
const reactee = await Users.findOne(note.userId);
diff --git a/src/services/note/read.ts b/src/services/note/read.ts
index 5a39ab30b7..35279db411 100644
--- a/src/services/note/read.ts
+++ b/src/services/note/read.ts
@@ -2,70 +2,54 @@ import { publishMainStream } from '../stream';
import { Note } from '../../models/entities/note';
import { User } from '../../models/entities/user';
import { NoteUnreads, Antennas, AntennaNotes, Users } from '../../models';
-import { Not, IsNull } from 'typeorm';
+import { Not, IsNull, In } from 'typeorm';
/**
- * Mark a note as read
+ * Mark notes as read
*/
export default async function(
userId: User['id'],
- noteId: Note['id']
+ noteIds: Note['id'][]
) {
async function careNoteUnreads() {
- const exist = await NoteUnreads.findOne({
- userId: userId,
- noteId: noteId,
- });
-
- if (!exist) return;
-
// Remove the record
await NoteUnreads.delete({
userId: userId,
- noteId: noteId,
+ noteId: In(noteIds),
});
- if (exist.isMentioned) {
- NoteUnreads.count({
- userId: userId,
- isMentioned: true
- }).then(mentionsCount => {
- if (mentionsCount === 0) {
- // 全て既読になったイベントを発行
- publishMainStream(userId, 'readAllUnreadMentions');
- }
- });
- }
+ NoteUnreads.count({
+ userId: userId,
+ isMentioned: true
+ }).then(mentionsCount => {
+ if (mentionsCount === 0) {
+ // 全て既読になったイベントを発行
+ publishMainStream(userId, 'readAllUnreadMentions');
+ }
+ });
- if (exist.isSpecified) {
- NoteUnreads.count({
- userId: userId,
- isSpecified: true
- }).then(specifiedCount => {
- if (specifiedCount === 0) {
- // 全て既読になったイベントを発行
- publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
- }
- });
- }
+ NoteUnreads.count({
+ userId: userId,
+ isSpecified: true
+ }).then(specifiedCount => {
+ if (specifiedCount === 0) {
+ // 全て既読になったイベントを発行
+ publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
+ }
+ });
- if (exist.noteChannelId) {
- NoteUnreads.count({
- userId: userId,
- noteChannelId: Not(IsNull())
- }).then(channelNoteCount => {
- if (channelNoteCount === 0) {
- // 全て既読になったイベントを発行
- publishMainStream(userId, 'readAllChannels');
- }
- });
- }
+ NoteUnreads.count({
+ userId: userId,
+ noteChannelId: Not(IsNull())
+ }).then(channelNoteCount => {
+ if (channelNoteCount === 0) {
+ // 全て既読になったイベントを発行
+ publishMainStream(userId, 'readAllChannels');
+ }
+ });
}
async function careAntenna() {
- const beforeUnread = await Users.getHasUnreadAntenna(userId);
- if (!beforeUnread) return;
-
const antennas = await Antennas.find({ userId });
await Promise.all(antennas.map(async antenna => {
@@ -78,7 +62,7 @@ export default async function(
await AntennaNotes.update({
antennaId: antenna.id,
- noteId: noteId
+ noteId: In(noteIds)
}, {
read: true
});
diff --git a/src/services/note/unread.ts b/src/services/note/unread.ts
index 6fd9ee2cfe..8e6fb4abe8 100644
--- a/src/services/note/unread.ts
+++ b/src/services/note/unread.ts
@@ -17,7 +17,7 @@ export default async function(userId: User['id'], note: Note, params: {
if (mute.map(m => m.muteeId).includes(note.userId)) return;
//#endregion
- const unread = await NoteUnreads.save({
+ const unread = {
id: genId(),
noteId: note.id,
userId: userId,
@@ -25,7 +25,9 @@ export default async function(userId: User['id'], note: Note, params: {
isMentioned: params.isMentioned,
noteChannelId: note.channelId,
noteUserId: note.userId,
- });
+ };
+
+ await NoteUnreads.insert(unread);
// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
setTimeout(async () => {
diff --git a/src/services/note/watch.ts b/src/services/note/watch.ts
index d3c9553696..966b7f0054 100644
--- a/src/services/note/watch.ts
+++ b/src/services/note/watch.ts
@@ -10,7 +10,7 @@ export default async (me: User['id'], note: Note) => {
return;
}
- await NoteWatchings.save({
+ await NoteWatchings.insert({
id: genId(),
createdAt: new Date(),
noteId: note.id,
diff --git a/src/services/register-or-fetch-instance-doc.ts b/src/services/register-or-fetch-instance-doc.ts
index 3501e20de1..2c39502288 100644
--- a/src/services/register-or-fetch-instance-doc.ts
+++ b/src/services/register-or-fetch-instance-doc.ts
@@ -3,10 +3,16 @@ import { Instances } from '../models';
import { federationChart } from './chart';
import { genId } from '../misc/gen-id';
import { toPuny } from '../misc/convert-host';
+import { Cache } from '../misc/cache';
+
+const cache = new Cache<Instance>(1000 * 60 * 60);
export async function registerOrFetchInstanceDoc(host: string): Promise<Instance> {
host = toPuny(host);
+ const cached = cache.get(host);
+ if (cached) return cached;
+
const index = await Instances.findOne({ host });
if (index == null) {
@@ -19,8 +25,10 @@ export async function registerOrFetchInstanceDoc(host: string): Promise<Instance
federationChart.update(true);
+ cache.set(host, i);
return i;
} else {
+ cache.set(host, index);
return index;
}
}
diff --git a/src/services/stream.ts b/src/services/stream.ts
index d833d700fe..75385847ce 100644
--- a/src/services/stream.ts
+++ b/src/services/stream.ts
@@ -20,6 +20,10 @@ class Publisher {
}));
}
+ public publishUserEvent = (userId: User['id'], type: string, value?: any): void => {
+ this.publish(`user:${userId}`, type, typeof value === 'undefined' ? null : value);
+ }
+
public publishBroadcastStream = (type: string, value?: any): void => {
this.publish('broadcast', type, typeof value === 'undefined' ? null : value);
}
@@ -84,6 +88,7 @@ const publisher = new Publisher();
export default publisher;
+export const publishUserEvent = publisher.publishUserEvent;
export const publishBroadcastStream = publisher.publishBroadcastStream;
export const publishMainStream = publisher.publishMainStream;
export const publishDriveStream = publisher.publishDriveStream;
diff --git a/src/services/update-hashtag.ts b/src/services/update-hashtag.ts
index 1dcb582791..3e22846731 100644
--- a/src/services/update-hashtag.ts
+++ b/src/services/update-hashtag.ts
@@ -86,7 +86,7 @@ export async function updateHashtag(user: User, tag: string, isUserAttached = fa
}
} else {
if (isUserAttached) {
- Hashtags.save({
+ Hashtags.insert({
id: genId(),
name: tag,
mentionedUserIds: [],
@@ -103,7 +103,7 @@ export async function updateHashtag(user: User, tag: string, isUserAttached = fa
attachedRemoteUsersCount: Users.isRemoteUser(user) ? 1 : 0,
} as Hashtag);
} else {
- Hashtags.save({
+ Hashtags.insert({
id: genId(),
name: tag,
mentionedUserIds: [user.id],
diff --git a/src/services/user-list/push.ts b/src/services/user-list/push.ts
index e67be4b027..ba54c04475 100644
--- a/src/services/user-list/push.ts
+++ b/src/services/user-list/push.ts
@@ -8,7 +8,7 @@ import { fetchProxyAccount } from '../../misc/fetch-proxy-account';
import createFollowing from '../following/create';
export async function pushUserToUserList(target: User, list: UserList) {
- await UserListJoinings.save({
+ await UserListJoinings.insert({
id: genId(),
createdAt: new Date(),
userId: target.id,
diff --git a/src/tsconfig.json b/src/tsconfig.json
new file mode 100644
index 0000000000..95cb35fc5f
--- /dev/null
+++ b/src/tsconfig.json
@@ -0,0 +1,39 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "noEmitOnError": false,
+ "noImplicitAny": true,
+ "noImplicitReturns": true,
+ "noUnusedParameters": false,
+ "noUnusedLocals": true,
+ "noFallthroughCasesInSwitch": true,
+ "declaration": false,
+ "sourceMap": true,
+ "target": "es2017",
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "removeComments": false,
+ "noLib": false,
+ "strict": true,
+ "strictNullChecks": true,
+ "strictPropertyInitialization": false,
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "typeRoots": [
+ "../node_modules/@types",
+ "./@types"
+ ],
+ "lib": [
+ "esnext"
+ ]
+ },
+ "compileOnSave": false,
+ "include": [
+ "./**/*.ts"
+ ],
+ "exclude": [
+ "./client/**/*.ts"
+ ]
+}