Merge branch 'develop'
This commit is contained in:
commit
7495206db2
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -35,6 +35,19 @@ mongodb:
|
||||||
8. master ブランチに戻す
|
8. master ブランチに戻す
|
||||||
9. enjoy
|
9. enjoy
|
||||||
|
|
||||||
|
11.4.0 (2019/04/25)
|
||||||
|
-------------------
|
||||||
|
### Improvements
|
||||||
|
* 検索でローカルの投稿のみに絞れるように
|
||||||
|
* 検索で特定のインスタンスの投稿のみに絞れるように
|
||||||
|
* 検索で特定のユーザーの投稿のみに絞れるように
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
* 投稿が増殖する問題を修正
|
||||||
|
* ストリームで過去の投稿が流れてくる問題を修正
|
||||||
|
* モバイル版のユーザーページで遷移してもユーザー名が変わらない問題を修正
|
||||||
|
* お知らせを切り替えても内容が変わらない問題を修正
|
||||||
|
|
||||||
11.3.1 (2019/04/24)
|
11.3.1 (2019/04/24)
|
||||||
-------------------
|
-------------------
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"author": "syuilo <i@syuilo.com>",
|
"author": "syuilo <i@syuilo.com>",
|
||||||
"version": "11.3.1",
|
"version": "11.4.0",
|
||||||
"codename": "daybreak",
|
"codename": "daybreak",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -23,6 +23,7 @@
|
||||||
"format": "gulp format"
|
"format": "gulp format"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@elastic/elasticsearch": "7.0.0-rc.2",
|
||||||
"@fortawesome/fontawesome-svg-core": "1.2.15",
|
"@fortawesome/fontawesome-svg-core": "1.2.15",
|
||||||
"@fortawesome/free-brands-svg-icons": "5.7.2",
|
"@fortawesome/free-brands-svg-icons": "5.7.2",
|
||||||
"@fortawesome/free-regular-svg-icons": "5.7.2",
|
"@fortawesome/free-regular-svg-icons": "5.7.2",
|
||||||
|
@ -35,7 +36,6 @@
|
||||||
"@types/dateformat": "3.0.0",
|
"@types/dateformat": "3.0.0",
|
||||||
"@types/deep-equal": "1.0.1",
|
"@types/deep-equal": "1.0.1",
|
||||||
"@types/double-ended-queue": "2.1.0",
|
"@types/double-ended-queue": "2.1.0",
|
||||||
"@types/elasticsearch": "5.0.32",
|
|
||||||
"@types/file-type": "10.9.1",
|
"@types/file-type": "10.9.1",
|
||||||
"@types/gulp": "4.0.6",
|
"@types/gulp": "4.0.6",
|
||||||
"@types/gulp-mocha": "0.0.32",
|
"@types/gulp-mocha": "0.0.32",
|
||||||
|
@ -113,7 +113,6 @@
|
||||||
"deep-equal": "1.0.1",
|
"deep-equal": "1.0.1",
|
||||||
"diskusage": "1.1.0",
|
"diskusage": "1.1.0",
|
||||||
"double-ended-queue": "2.1.0-0",
|
"double-ended-queue": "2.1.0-0",
|
||||||
"elasticsearch": "15.4.1",
|
|
||||||
"emojilib": "2.4.0",
|
"emojilib": "2.4.0",
|
||||||
"eslint": "5.16.0",
|
"eslint": "5.16.0",
|
||||||
"eslint-plugin-vue": "5.2.2",
|
"eslint-plugin-vue": "5.2.2",
|
||||||
|
|
31
src/client/app/common/scripts/gen-search-query.ts
Normal file
31
src/client/app/common/scripts/gen-search-query.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import parseAcct from '../../../../misc/acct/parse';
|
||||||
|
import { host as localHost } from '../../config';
|
||||||
|
|
||||||
|
export async function genSearchQuery(v: any, q: string) {
|
||||||
|
let host: string;
|
||||||
|
let userId: string;
|
||||||
|
if (q.split(' ').some(x => x.startsWith('@'))) {
|
||||||
|
for (const at of q.split(' ').filter(x => x.startsWith('@')).map(x => x.substr(1))) {
|
||||||
|
if (at.includes('.')) {
|
||||||
|
if (at === localHost || at === '.') {
|
||||||
|
host = null;
|
||||||
|
} else {
|
||||||
|
host = at;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const user = await v.$root.api('users/show', parseAcct(at)).catch(x => null);
|
||||||
|
if (user) {
|
||||||
|
userId = user.id;
|
||||||
|
} else {
|
||||||
|
// todo: show error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
query: q.split(' ').filter(x => !x.startsWith('/') && !x.startsWith('@')).join(' '),
|
||||||
|
host: host,
|
||||||
|
userId: userId
|
||||||
|
};
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import { faHistory } from '@fortawesome/free-solid-svg-icons';
|
||||||
export async function search(v: any, q: string) {
|
export async function search(v: any, q: string) {
|
||||||
q = q.trim();
|
q = q.trim();
|
||||||
|
|
||||||
if (q.startsWith('@')) {
|
if (q.startsWith('@') && !q.includes(' ')) {
|
||||||
v.$router.push(`/${q}`);
|
v.$router.push(`/${q}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,9 +60,9 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
init() {
|
async init() {
|
||||||
this.fetching = true;
|
this.fetching = true;
|
||||||
this.makePromise().then(x => {
|
await (this.makePromise()).then(x => {
|
||||||
if (Array.isArray(x)) {
|
if (Array.isArray(x)) {
|
||||||
this.us = x;
|
this.us = x;
|
||||||
} else {
|
} else {
|
||||||
|
@ -76,9 +76,9 @@ export default Vue.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchMoreUsers() {
|
async fetchMoreUsers() {
|
||||||
this.fetchingMoreUsers = true;
|
this.fetchingMoreUsers = true;
|
||||||
this.makePromise(this.cursor).then(x => {
|
await (this.makePromise(this.cursor)).then(x => {
|
||||||
this.us = this.us.concat(x.users);
|
this.us = this.us.concat(x.users);
|
||||||
this.cursor = x.cursor;
|
this.cursor = x.cursor;
|
||||||
this.fetchingMoreUsers = false;
|
this.fetchingMoreUsers = false;
|
||||||
|
|
|
@ -110,11 +110,11 @@ export default Vue.extend({
|
||||||
this.init();
|
this.init();
|
||||||
},
|
},
|
||||||
|
|
||||||
init() {
|
async init() {
|
||||||
this.queue = [];
|
this.queue = [];
|
||||||
this.notes = [];
|
this.notes = [];
|
||||||
this.fetching = true;
|
this.fetching = true;
|
||||||
this.makePromise().then(x => {
|
await (this.makePromise()).then(x => {
|
||||||
if (Array.isArray(x)) {
|
if (Array.isArray(x)) {
|
||||||
this.notes = x;
|
this.notes = x;
|
||||||
} else {
|
} else {
|
||||||
|
@ -129,10 +129,10 @@ export default Vue.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchMore() {
|
async fetchMore() {
|
||||||
if (!this.more || this.moreFetching) return;
|
if (!this.more || this.moreFetching) return;
|
||||||
this.moreFetching = true;
|
this.moreFetching = true;
|
||||||
this.makePromise(this.notes[this.notes.length - 1].id).then(x => {
|
await (this.makePromise(this.notes[this.notes.length - 1].id)).then(x => {
|
||||||
this.notes = this.notes.concat(x.notes);
|
this.notes = this.notes.concat(x.notes);
|
||||||
this.more = x.more;
|
this.more = x.more;
|
||||||
this.moreFetching = false;
|
this.moreFetching = false;
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import XColumn from './deck.column.vue';
|
import XColumn from './deck.column.vue';
|
||||||
import XNotes from './deck.notes.vue';
|
import XNotes from './deck.notes.vue';
|
||||||
|
import { genSearchQuery } from '../../../common/scripts/gen-search-query';
|
||||||
|
|
||||||
const limit = 20;
|
const limit = 20;
|
||||||
|
|
||||||
|
@ -25,10 +26,10 @@ export default Vue.extend({
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
makePromise: cursor => this.$root.api('notes/search', {
|
makePromise: async cursor => this.$root.api('notes/search', {
|
||||||
limit: limit + 1,
|
limit: limit + 1,
|
||||||
offset: cursor ? cursor : undefined,
|
offset: cursor ? cursor : undefined,
|
||||||
query: this.q
|
...(await genSearchQuery(this, this.q))
|
||||||
}).then(notes => {
|
}).then(notes => {
|
||||||
if (notes.length == limit + 1) {
|
if (notes.length == limit + 1) {
|
||||||
notes.pop();
|
notes.pop();
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<p class="fetching" v-if="fetching">{{ $t('fetching') }}<mk-ellipsis/></p>
|
<p class="fetching" v-if="fetching">{{ $t('fetching') }}<mk-ellipsis/></p>
|
||||||
<h1 v-if="!fetching">{{ announcements.length == 0 ? $t('no-broadcasts') : announcements[i].title }}</h1>
|
<h1 v-if="!fetching">{{ announcements.length == 0 ? $t('no-broadcasts') : announcements[i].title }}</h1>
|
||||||
<p v-if="!fetching">
|
<p v-if="!fetching">
|
||||||
<mfm v-if="announcements.length != 0" :text="announcements[i].text"/>
|
<mfm v-if="announcements.length != 0" :text="announcements[i].text" :key="i"/>
|
||||||
<img v-if="announcements.length != 0 && announcements[i].image" :src="announcements[i].image" alt="" style="display: block; max-height: 130px; max-width: 100%;"/>
|
<img v-if="announcements.length != 0 && announcements[i].image" :src="announcements[i].image" alt="" style="display: block; max-height: 130px; max-width: 100%;"/>
|
||||||
<template v-if="announcements.length == 0">{{ $t('have-a-nice-day') }}</template>
|
<template v-if="announcements.length == 0">{{ $t('have-a-nice-day') }}</template>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -105,9 +105,9 @@ export default Vue.extend({
|
||||||
this.init();
|
this.init();
|
||||||
},
|
},
|
||||||
|
|
||||||
init() {
|
async init() {
|
||||||
this.fetching = true;
|
this.fetching = true;
|
||||||
this.makePromise().then(x => {
|
await (this.makePromise()).then(x => {
|
||||||
if (Array.isArray(x)) {
|
if (Array.isArray(x)) {
|
||||||
this.notes = x;
|
this.notes = x;
|
||||||
} else {
|
} else {
|
||||||
|
@ -122,7 +122,7 @@ export default Vue.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchMore() {
|
async fetchMore() {
|
||||||
if (!this.more || this.moreFetching || this.notes.length === 0) return;
|
if (!this.more || this.moreFetching || this.notes.length === 0) return;
|
||||||
this.moreFetching = true;
|
this.moreFetching = true;
|
||||||
this.makePromise(this.notes[this.notes.length - 1].id).then(x => {
|
this.makePromise(this.notes[this.notes.length - 1].id).then(x => {
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import i18n from '../../../i18n';
|
import i18n from '../../../i18n';
|
||||||
import Progress from '../../../common/scripts/loading';
|
import Progress from '../../../common/scripts/loading';
|
||||||
|
import { genSearchQuery } from '../../../common/scripts/gen-search-query';
|
||||||
|
|
||||||
const limit = 20;
|
const limit = 20;
|
||||||
|
|
||||||
|
@ -21,10 +22,10 @@ export default Vue.extend({
|
||||||
i18n: i18n('desktop/views/pages/search.vue'),
|
i18n: i18n('desktop/views/pages/search.vue'),
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
makePromise: cursor => this.$root.api('notes/search', {
|
makePromise: async cursor => this.$root.api('notes/search', {
|
||||||
limit: limit + 1,
|
limit: limit + 1,
|
||||||
offset: cursor ? cursor : undefined,
|
offset: cursor ? cursor : undefined,
|
||||||
query: this.q
|
...(await genSearchQuery(this, this.q))
|
||||||
}).then(notes => {
|
}).then(notes => {
|
||||||
if (notes.length == limit + 1) {
|
if (notes.length == limit + 1) {
|
||||||
notes.pop();
|
notes.pop();
|
||||||
|
|
|
@ -106,9 +106,9 @@ export default Vue.extend({
|
||||||
this.init();
|
this.init();
|
||||||
},
|
},
|
||||||
|
|
||||||
init() {
|
async init() {
|
||||||
this.fetching = true;
|
this.fetching = true;
|
||||||
this.makePromise().then(x => {
|
await (this.makePromise()).then(x => {
|
||||||
if (Array.isArray(x)) {
|
if (Array.isArray(x)) {
|
||||||
this.notes = x;
|
this.notes = x;
|
||||||
} else {
|
} else {
|
||||||
|
@ -123,10 +123,10 @@ export default Vue.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchMore() {
|
async fetchMore() {
|
||||||
if (!this.more || this.moreFetching || this.notes.length === 0) return;
|
if (!this.more || this.moreFetching || this.notes.length === 0) return;
|
||||||
this.moreFetching = true;
|
this.moreFetching = true;
|
||||||
this.makePromise(this.notes[this.notes.length - 1].id).then(x => {
|
await (this.makePromise(this.notes[this.notes.length - 1].id)).then(x => {
|
||||||
this.notes = this.notes.concat(x.notes);
|
this.notes = this.notes.concat(x.notes);
|
||||||
this.more = x.more;
|
this.more = x.more;
|
||||||
this.moreFetching = false;
|
this.moreFetching = false;
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
<div class="announcements" v-if="announcements && announcements.length > 0">
|
<div class="announcements" v-if="announcements && announcements.length > 0">
|
||||||
<article v-for="announcement in announcements">
|
<article v-for="announcement in announcements">
|
||||||
<span v-html="announcement.title" class="title"></span>
|
<span v-html="announcement.title" class="title"></span>
|
||||||
<mfm :text="announcement.text"/>
|
<div><mfm :text="announcement.text"/></div>
|
||||||
<img v-if="announcement.image" :src="announcement.image" alt="" style="display: block; max-height: 120px; max-width: 100%;"/>
|
<img v-if="announcement.image" :src="announcement.image" alt="" style="display: block; max-height: 120px; max-width: 100%;"/>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import i18n from '../../../i18n';
|
import i18n from '../../../i18n';
|
||||||
import Progress from '../../../common/scripts/loading';
|
import Progress from '../../../common/scripts/loading';
|
||||||
|
import { genSearchQuery } from '../../../common/scripts/gen-search-query';
|
||||||
|
|
||||||
const limit = 20;
|
const limit = 20;
|
||||||
|
|
||||||
|
@ -19,10 +20,10 @@ export default Vue.extend({
|
||||||
i18n: i18n('mobile/views/pages/search.vue'),
|
i18n: i18n('mobile/views/pages/search.vue'),
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
makePromise: cursor => this.$root.api('notes/search', {
|
makePromise: async cursor => this.$root.api('notes/search', {
|
||||||
limit: limit + 1,
|
limit: limit + 1,
|
||||||
untilId: cursor ? cursor : undefined,
|
untilId: cursor ? cursor : undefined,
|
||||||
query: this.q
|
...(await genSearchQuery(this, this.q))
|
||||||
}).then(notes => {
|
}).then(notes => {
|
||||||
if (notes.length == limit + 1) {
|
if (notes.length == limit + 1) {
|
||||||
notes.pop();
|
notes.pop();
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<h1><mk-user-name :user="user" :key="user.id"/></h1>
|
<h1><mk-user-name :user="user" :key="user.id"/></h1>
|
||||||
<span class="username"><mk-acct :user="user" :detail="true" /></span>
|
<span class="username"><mk-acct :user="user" :detail="true" :key="user.id"/></span>
|
||||||
<span class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</span>
|
<span class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
|
|
|
@ -1,41 +1,30 @@
|
||||||
import * as elasticsearch from 'elasticsearch';
|
import * as elasticsearch from '@elastic/elasticsearch';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import Logger from '../services/logger';
|
|
||||||
|
|
||||||
const esLogger = new Logger('es');
|
|
||||||
|
|
||||||
const index = {
|
const index = {
|
||||||
settings: {
|
settings: {
|
||||||
analysis: {
|
analysis: {
|
||||||
normalizer: {
|
|
||||||
lowercase_normalizer: {
|
|
||||||
type: 'custom',
|
|
||||||
filter: ['lowercase']
|
|
||||||
}
|
|
||||||
},
|
|
||||||
analyzer: {
|
analyzer: {
|
||||||
bigram: {
|
ngram: {
|
||||||
tokenizer: 'bigram_tokenizer'
|
tokenizer: 'ngram'
|
||||||
}
|
|
||||||
},
|
|
||||||
tokenizer: {
|
|
||||||
bigram_tokenizer: {
|
|
||||||
type: 'nGram',
|
|
||||||
min_gram: 2,
|
|
||||||
max_gram: 2
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mappings: {
|
mappings: {
|
||||||
note: {
|
properties: {
|
||||||
properties: {
|
text: {
|
||||||
text: {
|
type: 'text',
|
||||||
type: 'text',
|
index: true,
|
||||||
index: true,
|
analyzer: 'ngram',
|
||||||
analyzer: 'bigram',
|
},
|
||||||
normalizer: 'lowercase_normalizer'
|
userId: {
|
||||||
}
|
type: 'keyword',
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
userHost: {
|
||||||
|
type: 'keyword',
|
||||||
|
index: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,31 +32,20 @@ const index = {
|
||||||
|
|
||||||
// Init ElasticSearch connection
|
// Init ElasticSearch connection
|
||||||
const client = config.elasticsearch ? new elasticsearch.Client({
|
const client = config.elasticsearch ? new elasticsearch.Client({
|
||||||
host: `${config.elasticsearch.host}:${config.elasticsearch.port}`
|
node: `http://${config.elasticsearch.host}:${config.elasticsearch.port}`,
|
||||||
|
pingTimeout: 30000
|
||||||
}) : null;
|
}) : null;
|
||||||
|
|
||||||
if (client) {
|
if (client) {
|
||||||
// Send a HEAD request
|
|
||||||
client.ping({
|
|
||||||
// Ping usually has a 3000ms timeout
|
|
||||||
requestTimeout: 30000
|
|
||||||
}, error => {
|
|
||||||
if (error) {
|
|
||||||
esLogger.error('elasticsearch is down!');
|
|
||||||
} else {
|
|
||||||
esLogger.succ('elasticsearch is available!');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
client.indices.exists({
|
client.indices.exists({
|
||||||
index: 'misskey'
|
index: 'misskey_note'
|
||||||
}).then(exist => {
|
}).then(exist => {
|
||||||
if (exist) return;
|
if (!exist.body) {
|
||||||
|
client.indices.create({
|
||||||
client.indices.create({
|
index: 'misskey_note',
|
||||||
index: 'misskey',
|
body: index
|
||||||
body: index
|
});
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -247,7 +247,7 @@ export async function resolveNote(value: string | IObject, resolver?: Resolver):
|
||||||
// リモートサーバーからフェッチしてきて登録
|
// リモートサーバーからフェッチしてきて登録
|
||||||
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
|
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
|
||||||
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
|
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
|
||||||
return await createNote(uri, resolver).catch(e => {
|
return await createNote(uri, resolver, true).catch(e => {
|
||||||
if (e.name === 'duplicated') {
|
if (e.name === 'duplicated') {
|
||||||
return fetchNote(uri).then(note => {
|
return fetchNote(uri).then(note => {
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
|
|
|
@ -101,6 +101,32 @@ async function fetchAny(uri: string) {
|
||||||
// /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
|
// /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
|
||||||
// これはDBに存在する可能性があるため再度DB検索
|
// これはDBに存在する可能性があるため再度DB検索
|
||||||
if (uri !== object.id) {
|
if (uri !== object.id) {
|
||||||
|
if (object.id.startsWith(config.url + '/')) {
|
||||||
|
const parts = object.id.split('/');
|
||||||
|
const id = parts.pop();
|
||||||
|
const type = parts.pop();
|
||||||
|
|
||||||
|
if (type === 'notes') {
|
||||||
|
const note = await Notes.findOne(id);
|
||||||
|
|
||||||
|
if (note) {
|
||||||
|
return {
|
||||||
|
type: 'Note',
|
||||||
|
object: await Notes.pack(note, null, { detail: true })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (type === 'users') {
|
||||||
|
const user = await Users.findOne(id);
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
return {
|
||||||
|
type: 'User',
|
||||||
|
object: await Users.pack(user, null, { detail: true })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [user, note] = await Promise.all([
|
const [user, note] = await Promise.all([
|
||||||
Users.findOne({ uri: object.id }),
|
Users.findOne({ uri: object.id }),
|
||||||
Notes.findOne({ uri: object.id })
|
Notes.findOne({ uri: object.id })
|
||||||
|
@ -120,7 +146,7 @@ async function fetchAny(uri: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['Note', 'Question', 'Article'].includes(object.type)) {
|
if (['Note', 'Question', 'Article'].includes(object.type)) {
|
||||||
const note = await createNote(object.id);
|
const note = await createNote(object.id, undefined, true);
|
||||||
return {
|
return {
|
||||||
type: 'Note',
|
type: 'Note',
|
||||||
object: await Notes.pack(note!, null, { detail: true })
|
object: await Notes.pack(note!, null, { detail: true })
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { ApiError } from '../../error';
|
||||||
import { Notes } from '../../../../models';
|
import { Notes } from '../../../../models';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { types, bool } from '../../../../misc/schema';
|
import { types, bool } from '../../../../misc/schema';
|
||||||
|
import { ID } from '../../../../misc/cafy-id';
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
desc: {
|
desc: {
|
||||||
|
@ -29,7 +30,17 @@ export const meta = {
|
||||||
offset: {
|
offset: {
|
||||||
validator: $.optional.num.min(0),
|
validator: $.optional.num.min(0),
|
||||||
default: 0
|
default: 0
|
||||||
}
|
},
|
||||||
|
|
||||||
|
host: {
|
||||||
|
validator: $.optional.nullable.str,
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
|
||||||
|
userId: {
|
||||||
|
validator: $.optional.nullable.type(ID),
|
||||||
|
default: null
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
|
@ -54,30 +65,51 @@ export const meta = {
|
||||||
export default define(meta, async (ps, me) => {
|
export default define(meta, async (ps, me) => {
|
||||||
if (es == null) throw new ApiError(meta.errors.searchingNotAvailable);
|
if (es == null) throw new ApiError(meta.errors.searchingNotAvailable);
|
||||||
|
|
||||||
const response = await es.search({
|
const userQuery = ps.userId != null ? [{
|
||||||
index: 'misskey',
|
term: {
|
||||||
type: 'note',
|
userId: ps.userId
|
||||||
|
}
|
||||||
|
}] : [];
|
||||||
|
|
||||||
|
const hostQuery = ps.userId == null ?
|
||||||
|
ps.host === null ? [{
|
||||||
|
bool: {
|
||||||
|
must_not: {
|
||||||
|
exists: {
|
||||||
|
field: 'userHost'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}] : ps.host !== undefined ? [{
|
||||||
|
term: {
|
||||||
|
userHost: ps.host
|
||||||
|
}
|
||||||
|
}] : []
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const result = await es.search({
|
||||||
|
index: 'misskey_note',
|
||||||
body: {
|
body: {
|
||||||
size: ps.limit!,
|
size: ps.limit!,
|
||||||
from: ps.offset,
|
from: ps.offset,
|
||||||
query: {
|
query: {
|
||||||
simple_query_string: {
|
bool: {
|
||||||
fields: ['text'],
|
must: [{
|
||||||
query: ps.query,
|
simple_query_string: {
|
||||||
default_operator: 'and'
|
fields: ['text'],
|
||||||
|
query: ps.query.toLowerCase(),
|
||||||
|
default_operator: 'and'
|
||||||
|
},
|
||||||
|
}, ...hostQuery, ...userQuery]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sort: [
|
sort: [{
|
||||||
{ _doc: 'desc' }
|
_doc: 'desc'
|
||||||
]
|
}]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.hits.total === 0) {
|
const hits = result.body.hits.hits.map((hit: any) => hit._id);
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const hits = response.hits.hits.map((hit: any) => hit.id);
|
|
||||||
|
|
||||||
if (hits.length === 0) return [];
|
if (hits.length === 0) return [];
|
||||||
|
|
||||||
|
|
|
@ -106,8 +106,6 @@ type Option = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async (user: User, data: Option, silent = false) => new Promise<Note>(async (res, rej) => {
|
export default async (user: User, data: Option, silent = false) => new Promise<Note>(async (res, rej) => {
|
||||||
const isFirstNote = user.notesCount === 0;
|
|
||||||
|
|
||||||
if (data.createdAt == null) data.createdAt = new Date();
|
if (data.createdAt == null) data.createdAt = new Date();
|
||||||
if (data.visibility == null) data.visibility = 'public';
|
if (data.visibility == null) data.visibility = 'public';
|
||||||
if (data.viaMobile == null) data.viaMobile = false;
|
if (data.viaMobile == null) data.viaMobile = false;
|
||||||
|
@ -195,8 +193,6 @@ export default async (user: User, data: Option, silent = false) => new Promise<N
|
||||||
// 統計を更新
|
// 統計を更新
|
||||||
notesChart.update(note, true);
|
notesChart.update(note, true);
|
||||||
perUserNotesChart.update(user, note, true);
|
perUserNotesChart.update(user, note, true);
|
||||||
// ローカルユーザーのチャートはタイムライン取得時に更新しているのでリモートユーザーの場合だけでよい
|
|
||||||
if (Users.isRemoteUser(user)) activeUsersChart.update(user);
|
|
||||||
|
|
||||||
// Register host
|
// Register host
|
||||||
if (Users.isRemoteUser(user)) {
|
if (Users.isRemoteUser(user)) {
|
||||||
|
@ -212,19 +208,6 @@ export default async (user: User, data: Option, silent = false) => new Promise<N
|
||||||
// Increment notes count (user)
|
// Increment notes count (user)
|
||||||
incNotesCountOfUser(user);
|
incNotesCountOfUser(user);
|
||||||
|
|
||||||
// 未読通知を作成
|
|
||||||
if (data.visibility == 'specified') {
|
|
||||||
if (data.visibleUsers == null) throw new Error('invalid param');
|
|
||||||
|
|
||||||
for (const u of data.visibleUsers) {
|
|
||||||
insertNoteUnread(u, note, true);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (const u of mentionedUsers) {
|
|
||||||
insertNoteUnread(u, note, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.reply) {
|
if (data.reply) {
|
||||||
saveReply(data.reply, note);
|
saveReply(data.reply, note);
|
||||||
}
|
}
|
||||||
|
@ -233,75 +216,91 @@ export default async (user: User, data: Option, silent = false) => new Promise<N
|
||||||
incRenoteCount(data.renote);
|
incRenoteCount(data.renote);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pack the note
|
|
||||||
const noteObj = await Notes.pack(note);
|
|
||||||
|
|
||||||
if (isFirstNote) {
|
|
||||||
(noteObj as any).isFirstNote = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
publishNotesStream(noteObj);
|
|
||||||
|
|
||||||
const nm = new NotificationManager(user, note);
|
|
||||||
const nmRelatedPromises = [];
|
|
||||||
|
|
||||||
createMentionedEvents(mentionedUsers, note, nm);
|
|
||||||
|
|
||||||
const noteActivity = await renderNoteOrRenoteActivity(data, note);
|
|
||||||
|
|
||||||
if (Users.isLocalUser(user)) {
|
|
||||||
deliverNoteToMentionedRemoteUsers(mentionedUsers, user, noteActivity);
|
|
||||||
}
|
|
||||||
|
|
||||||
const profile = await UserProfiles.findOne(user.id).then(ensure);
|
|
||||||
|
|
||||||
// If has in reply to note
|
|
||||||
if (data.reply) {
|
|
||||||
// Fetch watchers
|
|
||||||
nmRelatedPromises.push(notifyToWatchersOfReplyee(data.reply, user, nm));
|
|
||||||
|
|
||||||
// この投稿をWatchする
|
|
||||||
if (Users.isLocalUser(user) && profile.autoWatch) {
|
|
||||||
watch(user.id, data.reply);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通知
|
|
||||||
if (data.reply.userHost === null) {
|
|
||||||
nm.push(data.reply.userId, 'reply');
|
|
||||||
publishMainStream(data.reply.userId, 'reply', noteObj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it is renote
|
|
||||||
if (data.renote) {
|
|
||||||
const type = data.text ? 'quote' : 'renote';
|
|
||||||
|
|
||||||
// Notify
|
|
||||||
if (data.renote.userHost === null) {
|
|
||||||
nm.push(data.renote.userId, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch watchers
|
|
||||||
nmRelatedPromises.push(notifyToWatchersOfRenotee(data.renote, user, nm, type));
|
|
||||||
|
|
||||||
// この投稿をWatchする
|
|
||||||
if (Users.isLocalUser(user) && profile.autoWatch) {
|
|
||||||
watch(user.id, data.renote);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publish event
|
|
||||||
if ((user.id !== data.renote.userId) && data.renote.userHost === null) {
|
|
||||||
publishMainStream(data.renote.userId, 'renote', noteObj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
publish(user, note, data.reply, data.renote, noteActivity);
|
// ローカルユーザーのチャートはタイムライン取得時に更新しているのでリモートユーザーの場合だけでよい
|
||||||
}
|
if (Users.isRemoteUser(user)) activeUsersChart.update(user);
|
||||||
|
|
||||||
Promise.all(nmRelatedPromises).then(() => {
|
// 未読通知を作成
|
||||||
nm.deliver();
|
if (data.visibility == 'specified') {
|
||||||
});
|
if (data.visibleUsers == null) throw new Error('invalid param');
|
||||||
|
|
||||||
|
for (const u of data.visibleUsers) {
|
||||||
|
insertNoteUnread(u, note, true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const u of mentionedUsers) {
|
||||||
|
insertNoteUnread(u, note, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pack the note
|
||||||
|
const noteObj = await Notes.pack(note);
|
||||||
|
|
||||||
|
if (user.notesCount === 0) {
|
||||||
|
(noteObj as any).isFirstNote = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
publishNotesStream(noteObj);
|
||||||
|
|
||||||
|
const nm = new NotificationManager(user, note);
|
||||||
|
const nmRelatedPromises = [];
|
||||||
|
|
||||||
|
createMentionedEvents(mentionedUsers, note, nm);
|
||||||
|
|
||||||
|
const noteActivity = await renderNoteOrRenoteActivity(data, note);
|
||||||
|
|
||||||
|
if (Users.isLocalUser(user)) {
|
||||||
|
deliverNoteToMentionedRemoteUsers(mentionedUsers, user, noteActivity);
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = await UserProfiles.findOne(user.id).then(ensure);
|
||||||
|
|
||||||
|
// If has in reply to note
|
||||||
|
if (data.reply) {
|
||||||
|
// Fetch watchers
|
||||||
|
nmRelatedPromises.push(notifyToWatchersOfReplyee(data.reply, user, nm));
|
||||||
|
|
||||||
|
// この投稿をWatchする
|
||||||
|
if (Users.isLocalUser(user) && profile.autoWatch) {
|
||||||
|
watch(user.id, data.reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知
|
||||||
|
if (data.reply.userHost === null) {
|
||||||
|
nm.push(data.reply.userId, 'reply');
|
||||||
|
publishMainStream(data.reply.userId, 'reply', noteObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it is renote
|
||||||
|
if (data.renote) {
|
||||||
|
const type = data.text ? 'quote' : 'renote';
|
||||||
|
|
||||||
|
// Notify
|
||||||
|
if (data.renote.userHost === null) {
|
||||||
|
nm.push(data.renote.userId, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch watchers
|
||||||
|
nmRelatedPromises.push(notifyToWatchersOfRenotee(data.renote, user, nm, type));
|
||||||
|
|
||||||
|
// この投稿をWatchする
|
||||||
|
if (Users.isLocalUser(user) && profile.autoWatch) {
|
||||||
|
watch(user.id, data.renote);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish event
|
||||||
|
if ((user.id !== data.renote.userId) && data.renote.userHost === null) {
|
||||||
|
publishMainStream(data.renote.userId, 'renote', noteObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
publish(user, note, data.reply, data.renote, noteActivity);
|
||||||
|
|
||||||
|
Promise.all(nmRelatedPromises).then(() => {
|
||||||
|
nm.deliver();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Register to search database
|
// Register to search database
|
||||||
index(note);
|
index(note);
|
||||||
|
@ -436,11 +435,12 @@ function index(note: Note) {
|
||||||
if (note.text == null || config.elasticsearch == null) return;
|
if (note.text == null || config.elasticsearch == null) return;
|
||||||
|
|
||||||
es!.index({
|
es!.index({
|
||||||
index: 'misskey',
|
index: 'misskey_note',
|
||||||
type: 'note',
|
|
||||||
id: note.id.toString(),
|
id: note.id.toString(),
|
||||||
body: {
|
body: {
|
||||||
text: note.text
|
text: note.text.toLowerCase(),
|
||||||
|
userId: note.userId,
|
||||||
|
userHost: note.userHost
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue