From 4c6fb60dd25d7e2865fc7c4d97728593ffc3c902 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 1 Sep 2018 08:13:18 +0900 Subject: #2541 --- src/server/api/endpoints/sw/register.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'src/server/api') diff --git a/src/server/api/endpoints/sw/register.ts b/src/server/api/endpoints/sw/register.ts index 3414600048..503fc94654 100644 --- a/src/server/api/endpoints/sw/register.ts +++ b/src/server/api/endpoints/sw/register.ts @@ -1,6 +1,7 @@ import $ from 'cafy'; import Subscription from '../../../../models/sw-subscription'; import { ILocalUser } from '../../../../models/user'; +import config from '../../../../config'; export const meta = { requireCredential: true @@ -31,8 +32,11 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res, deletedAt: { $exists: false } }); - if (exist !== null) { - return res(); + if (exist != null) { + return res({ + state: 'already-subscribed', + key: config.sw.public_key + }); } await Subscription.insert({ @@ -42,5 +46,8 @@ export default async (params: any, user: ILocalUser) => new Promise(async (res, publickey: publickey }); - res(); + res({ + state: 'subscribed', + key: config.sw.public_key + }); }); -- cgit v1.2.3-freya From 57a63d38aaae33331c0272cbcbc3f5d254cfd93d Mon Sep 17 00:00:00 2001 From: mei23 Date: Sat, 1 Sep 2018 20:17:30 +0900 Subject: Send Update activity --- src/remote/activitypub/renderer/update.ts | 14 ++++++++++++ src/server/api/endpoints/i/update.ts | 4 ++++ src/services/i/update.ts | 38 +++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 src/remote/activitypub/renderer/update.ts create mode 100644 src/services/i/update.ts (limited to 'src/server/api') diff --git a/src/remote/activitypub/renderer/update.ts b/src/remote/activitypub/renderer/update.ts new file mode 100644 index 0000000000..cf9acc9acb --- /dev/null +++ b/src/remote/activitypub/renderer/update.ts @@ -0,0 +1,14 @@ +import config from '../../../config'; +import { ILocalUser } from '../../../models/user'; + +export default (object: any, user: ILocalUser) => { + const activity = { + id: `${config.url}/users/${user._id}#updates/${new Date().getTime()}`, + actor: `${config.url}/users/${user._id}`, + type: 'Update', + to: [ 'https://www.w3.org/ns/activitystreams#Public' ], + object + } as any; + + return activity; +}; diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index cdb4eb3f56..585339e249 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -5,6 +5,7 @@ import DriveFile from '../../../../models/drive-file'; import acceptAllFollowRequests from '../../../../services/following/requests/accept-all'; import { IApp } from '../../../../models/app'; import config from '../../../../config'; +import { publishToFollowers } from '../../../../services/i/update'; export const meta = { desc: { @@ -144,4 +145,7 @@ export default async (params: any, user: ILocalUser, app: IApp) => new Promise(a if (user.isLocked && isLocked === false) { acceptAllFollowRequests(user); } + + // フォロワーにUpdateを配信 + publishToFollowers(user._id); }); diff --git a/src/services/i/update.ts b/src/services/i/update.ts new file mode 100644 index 0000000000..25b55b0355 --- /dev/null +++ b/src/services/i/update.ts @@ -0,0 +1,38 @@ +import * as mongo from 'mongodb'; +import User, { isLocalUser, isRemoteUser } from '../../models/user'; +import Following from '../../models/following'; +import renderPerson from '../../remote/activitypub/renderer/person'; +import renderUpdate from '../../remote/activitypub/renderer/update'; +import packAp from '../../remote/activitypub/renderer'; +import { deliver } from '../../queue'; + +export async function publishToFollowers(userId: mongo.ObjectID) { + const user = await User.findOne({ + _id: userId + }); + + const followers = await Following.find({ + followeeId: user._id + }); + + const queue: string[] = []; + + // フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信 + if (isLocalUser(user)) { + followers.map(following => { + const follower = following._follower; + + if (isRemoteUser(follower)) { + const inbox = follower.sharedInbox || follower.inbox; + if (!queue.includes(inbox)) queue.push(inbox); + } + }); + + if (queue.length > 0) { + const content = packAp(renderUpdate(await renderPerson(user), user)); + queue.forEach(inbox => { + deliver(user, content, inbox); + }); + } + } +} -- cgit v1.2.3-freya From 291beb45fcb7a4c856232b12848ebad8267e2840 Mon Sep 17 00:00:00 2001 From: Aya Morisawa Date: Sat, 1 Sep 2018 23:12:51 +0900 Subject: Use string interpolation --- src/client/app/auth/views/index.vue | 2 +- src/client/app/boot.js | 2 +- src/client/app/common/scripts/streaming/stream.ts | 4 ++-- src/client/app/common/views/components/autocomplete.vue | 4 ++-- .../app/common/views/components/connect-failed.troubleshooter.vue | 2 +- src/client/app/common/views/components/url-preview.vue | 2 +- src/client/app/common/views/directives/autocomplete.ts | 4 ++-- src/client/app/common/views/filters/note.ts | 2 +- src/client/app/common/views/filters/user.ts | 2 +- src/client/app/common/views/pages/follow.vue | 2 +- src/client/app/desktop/views/components/drive.folder.vue | 2 +- src/client/app/desktop/views/components/drive.vue | 4 ++-- src/client/app/desktop/views/components/media-image.vue | 2 +- src/client/app/desktop/views/components/post-form.vue | 6 +++--- src/client/app/desktop/views/pages/drive.vue | 2 +- src/client/app/desktop/views/pages/games/reversi.vue | 4 ++-- src/client/app/desktop/views/pages/messaging-room.vue | 2 +- src/client/app/mobile/views/components/post-form.vue | 6 +++--- src/client/app/mobile/views/components/ui.nav.vue | 2 +- src/client/app/mobile/views/pages/drive.vue | 4 ++-- src/client/app/mobile/views/pages/followers.vue | 2 +- src/client/app/mobile/views/pages/following.vue | 2 +- src/client/app/mobile/views/pages/games/reversi.vue | 4 ++-- src/client/app/mobile/views/pages/settings.vue | 2 +- src/client/app/mobile/views/pages/user-lists.vue | 2 +- src/client/app/mobile/views/pages/user.vue | 2 +- src/daemons/notes-stats.ts | 2 +- src/daemons/server-stats.ts | 2 +- src/mfm/html-to-mfm.ts | 2 +- src/mfm/html.ts | 4 ++-- src/misc/fa.ts | 2 +- src/remote/activitypub/renderer/hashtag.ts | 2 +- src/server/activitypub.ts | 2 +- src/server/api/endpoints.ts | 2 +- src/server/api/stream/notes-stats.ts | 2 +- src/server/api/stream/server-stats.ts | 2 +- src/server/web/docs.ts | 2 +- src/server/web/views/user.pug | 2 +- src/services/drive/add-file.ts | 2 +- webpack.config.ts | 4 ++-- 40 files changed, 53 insertions(+), 53 deletions(-) (limited to 'src/server/api') diff --git a/src/client/app/auth/views/index.vue b/src/client/app/auth/views/index.vue index 609e758994..ba7df911e5 100644 --- a/src/client/app/auth/views/index.vue +++ b/src/client/app/auth/views/index.vue @@ -80,7 +80,7 @@ export default Vue.extend({ accepted() { this.state = 'accepted'; if (this.session.app.callbackUrl) { - location.href = this.session.app.callbackUrl + '?token=' + this.session.token; + location.href = `${this.session.app.callbackUrl}?token=${this.session.token}`; } } } diff --git a/src/client/app/boot.js b/src/client/app/boot.js index 54397c98c6..dd2cf93a89 100644 --- a/src/client/app/boot.js +++ b/src/client/app/boot.js @@ -94,7 +94,7 @@ // Get salt query const salt = localStorage.getItem('salt') - ? '?salt=' + localStorage.getItem('salt') + ? `?salt=${localStorage.getItem('salt')}` : ''; // Load an app script diff --git a/src/client/app/common/scripts/streaming/stream.ts b/src/client/app/common/scripts/streaming/stream.ts index fefa8e5ced..4ab78f1190 100644 --- a/src/client/app/common/scripts/streaming/stream.ts +++ b/src/client/app/common/scripts/streaming/stream.ts @@ -44,11 +44,11 @@ export default class Connection extends EventEmitter { const query = params ? Object.keys(params) - .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])) + .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`) .join('&') : null; - this.socket = new ReconnectingWebsocket(`${wsUrl}/${endpoint}${query ? '?' + query : ''}`); + this.socket = new ReconnectingWebsocket(`${wsUrl}/${endpoint}${query ? `?${query}` : ''}`); this.socket.addEventListener('open', this.onOpen); this.socket.addEventListener('close', this.onClose); this.socket.addEventListener('message', this.onMessage); diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue index b274eaa0a0..ea05afd6dc 100644 --- a/src/client/app/common/views/components/autocomplete.vue +++ b/src/client/app/common/views/components/autocomplete.vue @@ -125,7 +125,7 @@ export default Vue.extend({ } if (this.type == 'user') { - const cacheKey = 'autocomplete:user:' + this.q; + const cacheKey = `autocomplete:user:${this.q}`; const cache = sessionStorage.getItem(cacheKey); if (cache) { const users = JSON.parse(cache); @@ -148,7 +148,7 @@ export default Vue.extend({ this.hashtags = JSON.parse(localStorage.getItem('hashtags') || '[]'); this.fetching = false; } else { - const cacheKey = 'autocomplete:hashtag:' + this.q; + const cacheKey = `autocomplete:hashtag:${this.q}`; const cache = sessionStorage.getItem(cacheKey); if (cache) { const hashtags = JSON.parse(cache); diff --git a/src/client/app/common/views/components/connect-failed.troubleshooter.vue b/src/client/app/common/views/components/connect-failed.troubleshooter.vue index 6c23cc7969..f64cae6b4b 100644 --- a/src/client/app/common/views/components/connect-failed.troubleshooter.vue +++ b/src/client/app/common/views/components/connect-failed.troubleshooter.vue @@ -57,7 +57,7 @@ export default Vue.extend({ } // Check internet connection - fetch('https://google.com?rand=' + Math.random(), { + fetch(`https://google.com?rand=${Math.random()}`, { mode: 'no-cors' }).then(() => { this.internet = true; diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue index d5dda0c80a..e182e7f8cb 100644 --- a/src/client/app/common/views/components/url-preview.vue +++ b/src/client/app/common/views/components/url-preview.vue @@ -170,7 +170,7 @@ export default Vue.extend({ return; } - fetch('/url?url=' + encodeURIComponent(this.url)).then(res => { + fetch(`/url?url=${encodeURIComponent(this.url)}`).then(res => { res.json().then(info => { if (info.url == null) return; this.title = info.title; diff --git a/src/client/app/common/views/directives/autocomplete.ts b/src/client/app/common/views/directives/autocomplete.ts index b252cf5c1f..26bc13871d 100644 --- a/src/client/app/common/views/directives/autocomplete.ts +++ b/src/client/app/common/views/directives/autocomplete.ts @@ -191,7 +191,7 @@ class Autocomplete { const acct = renderAcct(value); // 挿入 - this.text = trimmedBefore + '@' + acct + ' ' + after; + this.text = `${trimmedBefore}@${acct} ${after}`; // キャレットを戻す this.vm.$nextTick(() => { @@ -207,7 +207,7 @@ class Autocomplete { const after = source.substr(caret); // 挿入 - this.text = trimmedBefore + '#' + value + ' ' + after; + this.text = `${trimmedBefore}#${value} ${after}`; // キャレットを戻す this.vm.$nextTick(() => { diff --git a/src/client/app/common/views/filters/note.ts b/src/client/app/common/views/filters/note.ts index a611dc8685..3c9c8b7485 100644 --- a/src/client/app/common/views/filters/note.ts +++ b/src/client/app/common/views/filters/note.ts @@ -1,5 +1,5 @@ import Vue from 'vue'; Vue.filter('notePage', note => { - return '/notes/' + note.id; + return `/notes/${note.id}`; }); diff --git a/src/client/app/common/views/filters/user.ts b/src/client/app/common/views/filters/user.ts index ca0910fc53..e5220229b7 100644 --- a/src/client/app/common/views/filters/user.ts +++ b/src/client/app/common/views/filters/user.ts @@ -11,5 +11,5 @@ Vue.filter('userName', user => { }); Vue.filter('userPage', (user, path?) => { - return '/@' + Vue.filter('acct')(user) + (path ? '/' + path : ''); + return `/@${Vue.filter('acct')(user)}${(path ? `/${path}` : '')}`; }); diff --git a/src/client/app/common/views/pages/follow.vue b/src/client/app/common/views/pages/follow.vue index 13d855d20a..ec74b3a9b9 100644 --- a/src/client/app/common/views/pages/follow.vue +++ b/src/client/app/common/views/pages/follow.vue @@ -1,6 +1,6 @@ @@ -100,7 +100,7 @@ export default Vue.extend({ isRenote(): boolean { return (this.note.renote && this.note.text == null && - this.note.mediaIds.length == 0 && + this.note.fileIds.length == 0 && this.note.poll == null); }, @@ -371,7 +371,7 @@ root(isDark) .mk-url-preview margin-top 8px - > .media + > .files > img display block max-width 100% diff --git a/src/client/app/desktop/views/pages/deck/deck.notes.vue b/src/client/app/desktop/views/pages/deck/deck.notes.vue index f7fca5de92..2e7e30f12a 100644 --- a/src/client/app/desktop/views/pages/deck/deck.notes.vue +++ b/src/client/app/desktop/views/pages/deck/deck.notes.vue @@ -127,7 +127,7 @@ export default Vue.extend({ prepend(note, silent = false) { //#region 弾く const isMyNote = note.userId == this.$store.state.i.id; - const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null; + const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null; if (this.$store.state.settings.showMyRenotes === false) { if (isMyNote && isPureRenote) { diff --git a/src/client/app/desktop/views/pages/deck/deck.tl.vue b/src/client/app/desktop/views/pages/deck/deck.tl.vue index a9e4d489c3..120ceb7fc2 100644 --- a/src/client/app/desktop/views/pages/deck/deck.tl.vue +++ b/src/client/app/desktop/views/pages/deck/deck.tl.vue @@ -96,7 +96,7 @@ export default Vue.extend({ (this.$refs.timeline as any).init(() => new Promise((res, rej) => { (this as any).api(this.endpoint, { limit: fetchLimit + 1, - mediaOnly: this.mediaOnly, + withFiles: this.mediaOnly, includeMyRenotes: this.$store.state.settings.showMyRenotes, includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, includeLocalRenotes: this.$store.state.settings.showLocalRenotes @@ -117,7 +117,7 @@ export default Vue.extend({ const promise = (this as any).api(this.endpoint, { limit: fetchLimit + 1, - mediaOnly: this.mediaOnly, + withFiles: this.mediaOnly, untilId: (this.$refs.timeline as any).tail().id, includeMyRenotes: this.$store.state.settings.showMyRenotes, includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, @@ -138,7 +138,7 @@ export default Vue.extend({ }, onNote(note) { - if (this.mediaOnly && note.media.length == 0) return; + if (this.mediaOnly && note.files.length == 0) return; // Prepend a note (this.$refs.timeline as any).prepend(note); diff --git a/src/client/app/desktop/views/pages/user/user.photos.vue b/src/client/app/desktop/views/pages/user/user.photos.vue index 64c537f1ed..c5cd9e24fe 100644 --- a/src/client/app/desktop/views/pages/user/user.photos.vue +++ b/src/client/app/desktop/views/pages/user/user.photos.vue @@ -24,12 +24,12 @@ export default Vue.extend({ mounted() { (this as any).api('users/notes', { userId: this.user.id, - withMedia: true, + withFiles: true, limit: 9 }).then(notes => { notes.forEach(note => { - note.media.forEach(media => { - if (this.images.length < 9) this.images.push(media); + note.files.forEach(file => { + if (this.images.length < 9) this.images.push(file); }); }); this.fetching = false; diff --git a/src/client/app/desktop/views/pages/user/user.timeline.vue b/src/client/app/desktop/views/pages/user/user.timeline.vue index 67987fcb94..54221380a7 100644 --- a/src/client/app/desktop/views/pages/user/user.timeline.vue +++ b/src/client/app/desktop/views/pages/user/user.timeline.vue @@ -66,7 +66,7 @@ export default Vue.extend({ limit: fetchLimit + 1, untilDate: this.date ? this.date.getTime() : undefined, includeReplies: this.mode == 'with-replies', - withMedia: this.mode == 'with-media' + withFiles: this.mode == 'with-media' }).then(notes => { if (notes.length == fetchLimit + 1) { notes.pop(); @@ -86,7 +86,7 @@ export default Vue.extend({ userId: this.user.id, limit: fetchLimit + 1, includeReplies: this.mode == 'with-replies', - withMedia: this.mode == 'with-media', + withFiles: this.mode == 'with-media', untilId: (this.$refs.timeline as any).tail().id }); diff --git a/src/client/app/desktop/views/widgets/trends.vue b/src/client/app/desktop/views/widgets/trends.vue index c33bf2f2f2..aeaab63ac4 100644 --- a/src/client/app/desktop/views/widgets/trends.vue +++ b/src/client/app/desktop/views/widgets/trends.vue @@ -49,7 +49,7 @@ export default define({ offset: this.offset, renote: false, reply: false, - media: false, + file: false, poll: false }).then(notes => { const note = notes ? notes[0] : null; diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue index 786e57bb22..10ff3fcc09 100644 --- a/src/client/app/mobile/views/components/note-detail.vue +++ b/src/client/app/mobile/views/components/note-detail.vue @@ -40,8 +40,8 @@ (%i18n:@deleted%) -
- +
+
@@ -113,7 +113,7 @@ export default Vue.extend({ isRenote(): boolean { return (this.note.renote && this.note.text == null && - this.note.mediaIds.length == 0 && + this.note.fileIds.length == 0 && this.note.poll == null); }, @@ -369,7 +369,7 @@ root(isDark) > .mk-url-preview margin-top 8px - > .media + > .files > img display block max-width 100% diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue index 258433cb3f..9bd4a83ecb 100644 --- a/src/client/app/mobile/views/components/note.vue +++ b/src/client/app/mobile/views/components/note.vue @@ -28,8 +28,8 @@ RP:
-
- +
+
@@ -90,7 +90,7 @@ export default Vue.extend({ isRenote(): boolean { return (this.note.renote && this.note.text == null && - this.note.mediaIds.length == 0 && + this.note.fileIds.length == 0 && this.note.poll == null); }, @@ -414,7 +414,7 @@ root(isDark) .mk-url-preview margin-top 8px - > .media + > .files > img display block max-width 100% diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue index 714e521c0f..ce2670dc52 100644 --- a/src/client/app/mobile/views/components/notes.vue +++ b/src/client/app/mobile/views/components/notes.vue @@ -125,7 +125,7 @@ export default Vue.extend({ prepend(note, silent = false) { //#region 弾く const isMyNote = note.userId == this.$store.state.i.id; - const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null; + const isPureRenote = note.renoteId != null && note.text == null && note.fileIds.length == 0 && note.poll == null; if (this.$store.state.settings.showMyRenotes === false) { if (isMyNote && isPureRenote) { diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue index 8b1f7b08c8..644e27cce8 100644 --- a/src/client/app/mobile/views/components/post-form.vue +++ b/src/client/app/mobile/views/components/post-form.vue @@ -200,12 +200,12 @@ export default Vue.extend({ attachMedia(driveFile) { this.files.push(driveFile); - this.$emit('change-attached-media', this.files); + this.$emit('change-attached-files', this.files); }, detachMedia(file) { this.files = this.files.filter(x => x.id != file.id); - this.$emit('change-attached-media', this.files); + this.$emit('change-attached-files', this.files); }, onChangeFile() { @@ -269,7 +269,7 @@ export default Vue.extend({ this.text = ''; this.files = []; this.poll = false; - this.$emit('change-attached-media'); + this.$emit('change-attached-files'); }, post() { @@ -277,7 +277,7 @@ export default Vue.extend({ const viaMobile = this.$store.state.settings.disableViaMobile !== true; (this as any).api('notes/create', { text: this.text == '' ? undefined : this.text, - mediaIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined, + fileIds: this.files.length > 0 ? this.files.map(f => f.id) : undefined, replyId: this.reply ? this.reply.id : undefined, renoteId: this.renote ? this.renote.id : undefined, poll: this.poll ? (this.$refs.poll as any).get() : undefined, diff --git a/src/client/app/mobile/views/components/sub-note-content.vue b/src/client/app/mobile/views/components/sub-note-content.vue index a4ce49786e..4d0aa25f34 100644 --- a/src/client/app/mobile/views/components/sub-note-content.vue +++ b/src/client/app/mobile/views/components/sub-note-content.vue @@ -7,9 +7,9 @@ RP: ...
-
- ({{ '%i18n:@media-count%'.replace('{}', note.media.length) }}) - +
+ ({{ '%i18n:@media-count%'.replace('{}', note.files.length) }}) +
%i18n:@poll% diff --git a/src/client/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue index 6be675c0a7..7cd23d6655 100644 --- a/src/client/app/mobile/views/components/user-timeline.vue +++ b/src/client/app/mobile/views/components/user-timeline.vue @@ -41,7 +41,7 @@ export default Vue.extend({ (this.$refs.timeline as any).init(() => new Promise((res, rej) => { (this as any).api('users/notes', { userId: this.user.id, - withMedia: this.withMedia, + withFiles: this.withMedia, limit: fetchLimit + 1 }).then(notes => { if (notes.length == fetchLimit + 1) { @@ -62,7 +62,7 @@ export default Vue.extend({ const promise = (this as any).api('users/notes', { userId: this.user.id, - withMedia: this.withMedia, + withFiles: this.withMedia, limit: fetchLimit + 1, untilId: (this.$refs.timeline as any).tail().id }); diff --git a/src/client/app/mobile/views/pages/user/home.photos.vue b/src/client/app/mobile/views/pages/user/home.photos.vue index 73ff1d5173..e9025ec816 100644 --- a/src/client/app/mobile/views/pages/user/home.photos.vue +++ b/src/client/app/mobile/views/pages/user/home.photos.vue @@ -26,7 +26,7 @@ export default Vue.extend({ mounted() { (this as any).api('users/notes', { userId: this.user.id, - withMedia: true, + withFiles: true, limit: 6 }).then(notes => { notes.forEach(note => { diff --git a/src/docs/api/entities/note.yaml b/src/docs/api/entities/note.yaml index cae9a53f82..6654be2b02 100644 --- a/src/docs/api/entities/note.yaml +++ b/src/docs/api/entities/note.yaml @@ -33,19 +33,19 @@ props: ja-JP: "投稿の本文" en-US: "The text of this note" - mediaIds: + fileIds: type: "id(DriveFile)[]" optional: true desc: - ja-JP: "添付されているメディアのID (なければレスポンスでは空配列)" - en-US: "The IDs of the attached media (empty array for response if no media is attached)" + ja-JP: "添付されているファイルのID (なければレスポンスでは空配列)" + en-US: "The IDs of the attached files (empty array for response if no files is attached)" - media: + files: type: "entity(DriveFile)[]" optional: true desc: - ja-JP: "添付されているメディア" - en-US: "The attached media" + ja-JP: "添付されているファイル" + en-US: "The attached files" userId: type: "id(User)" diff --git a/src/misc/get-note-summary.ts b/src/misc/get-note-summary.ts index ec7c74cf9f..3c6f2dd3d6 100644 --- a/src/misc/get-note-summary.ts +++ b/src/misc/get-note-summary.ts @@ -16,9 +16,9 @@ const summarize = (note: any): string => { // 本文 summary += note.text ? note.text : ''; - // メディアが添付されているとき - if (note.media.length != 0) { - summary += ` (${note.media.length}つのメディア)`; + // ファイルが添付されているとき + if (note.files.length != 0) { + summary += ` (${note.files.length}つのファイル)`; } // 投票が添付されているとき diff --git a/src/misc/is-quote.ts b/src/misc/is-quote.ts index 420f03a489..a99b8f6434 100644 --- a/src/misc/is-quote.ts +++ b/src/misc/is-quote.ts @@ -1,5 +1,5 @@ import { INote } from '../models/note'; export default function(note: INote): boolean { - return note.renoteId != null && (note.text != null || note.poll != null || (note.mediaIds != null && note.mediaIds.length > 0)); + return note.renoteId != null && (note.text != null || note.poll != null || (note.fileIds != null && note.fileIds.length > 0)); } diff --git a/src/models/drive-file.ts b/src/models/drive-file.ts index 698ef092a6..215b49b305 100644 --- a/src/models/drive-file.ts +++ b/src/models/drive-file.ts @@ -92,7 +92,7 @@ export async function deleteDriveFile(driveFile: string | mongo.ObjectID | IDriv // このDriveFileを添付しているNoteをすべて削除 await Promise.all(( - await Note.find({ mediaIds: d._id }) + await Note.find({ fileIds: d._id }) ).map(x => deleteNote(x))); // このDriveFileを添付しているMessagingMessageをすべて削除 diff --git a/src/models/note.ts b/src/models/note.ts index 9d2e23d901..98d37caf22 100644 --- a/src/models/note.ts +++ b/src/models/note.ts @@ -6,7 +6,7 @@ import { IUser, pack as packUser } from './user'; import { pack as packApp } from './app'; import PollVote, { deletePollVote } from './poll-vote'; import Reaction, { deleteNoteReaction } from './note-reaction'; -import { pack as packFile } from './drive-file'; +import { pack as packFile, IDriveFile } from './drive-file'; import NoteWatching, { deleteNoteWatching } from './note-watching'; import NoteReaction from './note-reaction'; import Favorite, { deleteFavorite } from './favorite'; @@ -17,9 +17,20 @@ const Note = db.get('notes'); Note.createIndex('uri', { sparse: true, unique: true }); Note.createIndex('userId'); Note.createIndex('tagsLower'); +Note.createIndex('_files.contentType'); Note.createIndex({ createdAt: -1 }); + +// 後方互換性のため +Note.update({}, { + $rename: { + mediaIds: 'fileIds' + } +}, { + multi: true +}); + export default Note; export function isValidText(text: string): boolean { @@ -34,7 +45,7 @@ export type INote = { _id: mongo.ObjectID; createdAt: Date; deletedAt: Date; - mediaIds: mongo.ObjectID[]; + fileIds: mongo.ObjectID[]; replyId: mongo.ObjectID; renoteId: mongo.ObjectID; poll: { @@ -92,6 +103,7 @@ export type INote = { inbox?: string; }; _replyIds?: mongo.ObjectID[]; + _files?: IDriveFile[]; }; /** @@ -271,11 +283,15 @@ export const pack = async ( _note.app = packApp(_note.appId); } - // Populate media - _note.media = hide ? [] : Promise.all(_note.mediaIds.map((fileId: mongo.ObjectID) => + // Populate files + _note.files = hide ? [] : Promise.all(_note.fileIds.map((fileId: mongo.ObjectID) => packFile(fileId) )); + // 後方互換性のため + _note.mediaIds = _note.fileIds; + _note.media = _note.files; + // When requested a detailed note data if (opts.detail) { //#region 重いので廃止 @@ -344,7 +360,7 @@ export const pack = async ( } if (hide) { - _note.mediaIds = []; + _note.fileIds = []; _note.text = null; _note.poll = null; _note.cw = null; diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts index 1dfeebfdf7..97188b44a6 100644 --- a/src/remote/activitypub/models/note.ts +++ b/src/remote/activitypub/models/note.ts @@ -78,11 +78,11 @@ export async function createNote(value: any, resolver?: Resolver, silent = false } //#endergion - // 添付メディア + // 添付ファイル // TODO: attachmentは必ずしもImageではない // TODO: attachmentは必ずしも配列ではない // Noteがsensitiveなら添付もsensitiveにする - const media = note.attachment + const files = note.attachment .map(attach => attach.sensitive = note.sensitive) ? await Promise.all(note.attachment.map(x => resolveImage(actor, x))) : []; @@ -100,7 +100,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false return await post(actor, { createdAt: new Date(note.published), - media, + files: files, reply, renote: undefined, cw: note.summary, diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts index 1d169d3088..6b30324ae7 100644 --- a/src/remote/activitypub/renderer/note.ts +++ b/src/remote/activitypub/renderer/note.ts @@ -8,8 +8,8 @@ import User from '../../../models/user'; import toHtml from '../misc/get-note-html'; export default async function renderNote(note: INote, dive = true): Promise { - const promisedFiles: Promise = note.mediaIds - ? DriveFile.find({ _id: { $in: note.mediaIds } }) + const promisedFiles: Promise = note.fileIds + ? DriveFile.find({ _id: { $in: note.fileIds } }) : Promise.resolve([]); let inReplyTo; diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts index 37df190880..cc7e55b5df 100644 --- a/src/server/activitypub/outbox.ts +++ b/src/server/activitypub/outbox.ts @@ -58,7 +58,7 @@ export default async (ctx: Router.IRouterContext) => { $or: [{ text: { $ne: null } }, { - mediaIds: { $ne: [] } + fileIds: { $ne: [] } }] }] } as any; diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts index 029bc1a95e..4d15e9483f 100644 --- a/src/server/api/endpoints/notes.ts +++ b/src/server/api/endpoints/notes.ts @@ -20,9 +20,9 @@ export default (params: any) => new Promise(async (res, rej) => { const [renote, renoteErr] = $.bool.optional.get(params.renote); if (renoteErr) return rej('invalid renote param'); - // Get 'media' parameter - const [media, mediaErr] = $.bool.optional.get(params.media); - if (mediaErr) return rej('invalid media param'); + // Get 'files' parameter + const [files, filesErr] = $.bool.optional.get(params.files); + if (filesErr) return rej('invalid files param'); // Get 'poll' parameter const [poll, pollErr] = $.bool.optional.get(params.poll); @@ -79,8 +79,8 @@ export default (params: any) => new Promise(async (res, rej) => { query.renoteId = renote ? { $exists: true, $ne: null } : null; } - if (media != undefined) { - query.mediaIds = media ? { $exists: true, $ne: null } : []; + if (files != undefined) { + query.fileIds = files ? { $exists: true, $ne: null } : []; } if (poll != undefined) { diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts index 04f5f7562e..47b53c943b 100644 --- a/src/server/api/endpoints/notes/create.ts +++ b/src/server/api/endpoints/notes/create.ts @@ -71,9 +71,15 @@ export const meta = { ref: 'geo' }), + fileIds: $.arr($.type(ID)).optional.unique().range(1, 4).note({ + desc: { + 'ja-JP': '添付するファイル' + } + }), + mediaIds: $.arr($.type(ID)).optional.unique().range(1, 4).note({ desc: { - 'ja-JP': '添付するメディア' + 'ja-JP': '添付するファイル (このパラメータは廃止予定です。代わりに fileIds を使ってください。)' } }), @@ -124,15 +130,16 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async ( } let files: IDriveFile[] = []; - if (ps.mediaIds !== undefined) { + const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null; + if (fileIds != null) { // Fetch files // forEach だと途中でエラーなどがあっても return できないので // 敢えて for を使っています。 - for (const mediaId of ps.mediaIds) { + for (const fileId of fileIds) { // Fetch file // SELECT _id const entity = await DriveFile.findOne({ - _id: mediaId, + _id: fileId, 'metadata.userId': user._id }); @@ -155,7 +162,7 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async ( if (renote == null) { return rej('renoteee is not found'); - } else if (renote.renoteId && !renote.text && !renote.mediaIds) { + } else if (renote.renoteId && !renote.text && !renote.fileIds) { return rej('cannot renote to renote'); } } @@ -176,7 +183,7 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async ( } // 返信対象が引用でないRenoteだったらエラー - if (reply.renoteId && !reply.text && !reply.mediaIds) { + if (reply.renoteId && !reply.text && !reply.fileIds) { return rej('cannot reply to renote'); } } @@ -191,13 +198,13 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async ( // テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー if ((ps.text === undefined || ps.text === null) && files === null && renote === null && ps.poll === undefined) { - return rej('text, mediaIds, renoteId or poll is required'); + return rej('text, fileIds, renoteId or poll is required'); } // 投稿を作成 const note = await create(user, { createdAt: new Date(), - media: files, + files: files, poll: ps.poll, text: ps.text, reply, diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts index 8f7233e308..554245a0f4 100644 --- a/src/server/api/endpoints/notes/global-timeline.ts +++ b/src/server/api/endpoints/notes/global-timeline.ts @@ -33,9 +33,9 @@ export default async (params: any, user: ILocalUser) => { throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; } - // Get 'mediaOnly' parameter - const [mediaOnly, mediaOnlyErr] = $.bool.optional.get(params.mediaOnly); - if (mediaOnlyErr) throw 'invalid mediaOnly param'; + // Get 'withFiles' parameter + const [withFiles, withFilesErr] = $.bool.optional.get(params.withFiles); + if (withFilesErr) throw 'invalid withFiles param'; // ミュートしているユーザーを取得 const mutedUserIds = user ? (await Mute.find({ @@ -68,8 +68,8 @@ export default async (params: any, user: ILocalUser) => { }; } - if (mediaOnly) { - query.mediaIds = { $exists: true, $ne: [] }; + if (withFiles) { + query.fileIds = { $exists: true, $ne: [] }; } if (sinceId) { diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts index 2dbb1190c1..1060792683 100644 --- a/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -66,7 +66,7 @@ export const meta = { } }), - mediaOnly: $.bool.optional.note({ + withFiles: $.bool.optional.note({ desc: { 'ja-JP': 'true にすると、メディアが添付された投稿だけ取得します' } @@ -164,7 +164,7 @@ export default async (params: any, user: ILocalUser) => { }, { text: { $ne: null } }, { - mediaIds: { $ne: [] } + fileIds: { $ne: [] } }, { poll: { $ne: null } }] @@ -180,7 +180,7 @@ export default async (params: any, user: ILocalUser) => { }, { text: { $ne: null } }, { - mediaIds: { $ne: [] } + fileIds: { $ne: [] } }, { poll: { $ne: null } }] @@ -196,16 +196,16 @@ export default async (params: any, user: ILocalUser) => { }, { text: { $ne: null } }, { - mediaIds: { $ne: [] } + fileIds: { $ne: [] } }, { poll: { $ne: null } }] }); } - if (ps.mediaOnly) { + if (ps.withFiles) { query.$and.push({ - mediaIds: { $exists: true, $ne: [] } + fileIds: { $exists: true, $ne: [] } }); } diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts index bbcc6303ca..018e636ab5 100644 --- a/src/server/api/endpoints/notes/local-timeline.ts +++ b/src/server/api/endpoints/notes/local-timeline.ts @@ -33,9 +33,9 @@ export default async (params: any, user: ILocalUser) => { throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; } - // Get 'mediaOnly' parameter - const [mediaOnly, mediaOnlyErr] = $.bool.optional.get(params.mediaOnly); - if (mediaOnlyErr) throw 'invalid mediaOnly param'; + // Get 'withFiles' parameter + const [withFiles, withFilesErr] = $.bool.optional.get(params.withFiles); + if (withFilesErr) throw 'invalid withFiles param'; // ミュートしているユーザーを取得 const mutedUserIds = user ? (await Mute.find({ @@ -69,8 +69,8 @@ export default async (params: any, user: ILocalUser) => { }; } - if (mediaOnly) { - query.mediaIds = { $exists: true, $ne: [] }; + if (withFiles) { + query.fileIds = { $exists: true, $ne: [] }; } if (sinceId) { diff --git a/src/server/api/endpoints/notes/search_by_tag.ts b/src/server/api/endpoints/notes/search_by_tag.ts index e092275fe8..317a0726d3 100644 --- a/src/server/api/endpoints/notes/search_by_tag.ts +++ b/src/server/api/endpoints/notes/search_by_tag.ts @@ -247,7 +247,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => if (media != null) { if (media) { push({ - mediaIds: { + fileIds: { $exists: true, $ne: null } @@ -255,11 +255,11 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => } else { push({ $or: [{ - mediaIds: { + fileIds: { $exists: false } }, { - mediaIds: null + fileIds: null }] }); } diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts index 099bf2010b..145f648c56 100644 --- a/src/server/api/endpoints/notes/timeline.ts +++ b/src/server/api/endpoints/notes/timeline.ts @@ -67,7 +67,7 @@ export const meta = { } }), - mediaOnly: $.bool.optional.note({ + withFiles: $.bool.optional.note({ desc: { 'ja-JP': 'true にすると、メディアが添付された投稿だけ取得します' } @@ -154,7 +154,7 @@ export default async (params: any, user: ILocalUser) => { }, { text: { $ne: null } }, { - mediaIds: { $ne: [] } + fileIds: { $ne: [] } }, { poll: { $ne: null } }] @@ -170,7 +170,7 @@ export default async (params: any, user: ILocalUser) => { }, { text: { $ne: null } }, { - mediaIds: { $ne: [] } + fileIds: { $ne: [] } }, { poll: { $ne: null } }] @@ -186,16 +186,16 @@ export default async (params: any, user: ILocalUser) => { }, { text: { $ne: null } }, { - mediaIds: { $ne: [] } + fileIds: { $ne: [] } }, { poll: { $ne: null } }] }); } - if (ps.mediaOnly) { + if (ps.withFiles) { query.$and.push({ - mediaIds: { $exists: true, $ne: [] } + fileIds: { $exists: true, $ne: [] } }); } diff --git a/src/server/api/endpoints/notes/trend.ts b/src/server/api/endpoints/notes/trend.ts index 7a0a098f28..9f55ed3243 100644 --- a/src/server/api/endpoints/notes/trend.ts +++ b/src/server/api/endpoints/notes/trend.ts @@ -52,7 +52,7 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = } if (media != undefined) { - query.mediaIds = media ? { $exists: true, $ne: null } : null; + query.fileIds = media ? { $exists: true, $ne: null } : null; } if (poll != undefined) { diff --git a/src/server/api/endpoints/notes/user-list-timeline.ts b/src/server/api/endpoints/notes/user-list-timeline.ts index a7b43014ed..e00a7de371 100644 --- a/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/src/server/api/endpoints/notes/user-list-timeline.ts @@ -73,7 +73,7 @@ export const meta = { } }), - mediaOnly: $.bool.optional.note({ + withFiles: $.bool.optional.note({ desc: { 'ja-JP': 'true にすると、メディアが添付された投稿だけ取得します' } @@ -160,7 +160,7 @@ export default async (params: any, user: ILocalUser) => { }, { text: { $ne: null } }, { - mediaIds: { $ne: [] } + fileIds: { $ne: [] } }, { poll: { $ne: null } }] @@ -176,7 +176,7 @@ export default async (params: any, user: ILocalUser) => { }, { text: { $ne: null } }, { - mediaIds: { $ne: [] } + fileIds: { $ne: [] } }, { poll: { $ne: null } }] @@ -192,16 +192,16 @@ export default async (params: any, user: ILocalUser) => { }, { text: { $ne: null } }, { - mediaIds: { $ne: [] } + fileIds: { $ne: [] } }, { poll: { $ne: null } }] }); } - if (ps.mediaOnly) { + if (ps.withFiles) { query.$and.push({ - mediaIds: { $exists: true, $ne: [] } + fileIds: { $exists: true, $ne: [] } }); } diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts index ff7855bde0..d894e52dba 100644 --- a/src/server/api/endpoints/users/notes.ts +++ b/src/server/api/endpoints/users/notes.ts @@ -27,9 +27,9 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => const [includeReplies = true, includeRepliesErr] = $.bool.optional.get(params.includeReplies); if (includeRepliesErr) return rej('invalid includeReplies param'); - // Get 'withMedia' parameter - const [withMedia = false, withMediaErr] = $.bool.optional.get(params.withMedia); - if (withMediaErr) return rej('invalid withMedia param'); + // Get 'withFiles' parameter + const [withFiles = false, withFilesErr] = $.bool.optional.get(params.withFiles); + if (withFilesErr) return rej('invalid withFiles param'); // Get 'limit' parameter const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit); @@ -104,8 +104,8 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => query.replyId = null; } - if (withMedia) { - query.mediaIds = { + if (withFiles) { + query.fileIds = { $exists: true, $ne: [] }; diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 63e3557828..11e3755863 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -84,7 +84,7 @@ type Option = { text?: string; reply?: INote; renote?: INote; - media?: IDriveFile[]; + files?: IDriveFile[]; geo?: any; poll?: any; viaMobile?: boolean; @@ -135,7 +135,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< const mentionedUsers = await extractMentionedUsers(tokens); - const note = await insertNote(user, data, tokens, tags, mentionedUsers); + const note = await insertNote(user, data, tags, mentionedUsers); res(note); @@ -309,10 +309,10 @@ async function publish(user: IUser, note: INote, noteObj: any, reply: INote, ren publishToUserLists(note, noteObj); } -async function insertNote(user: IUser, data: Option, tokens: ReturnType, tags: string[], mentionedUsers: IUser[]) { +async function insertNote(user: IUser, data: Option, tags: string[], mentionedUsers: IUser[]) { const insert: any = { createdAt: data.createdAt, - mediaIds: data.media ? data.media.map(file => file._id) : [], + fileIds: data.files ? data.files.map(file => file._id) : [], replyId: data.reply ? data.reply._id : null, renoteId: data.renote ? data.renote._id : null, text: data.text, @@ -347,7 +347,8 @@ async function insertNote(user: IUser, data: Option, tokens: ReturnType Date: Wed, 5 Sep 2018 19:35:57 +0900 Subject: Fix parameter name --- src/server/api/endpoints/notes/search_by_tag.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'src/server/api') diff --git a/src/server/api/endpoints/notes/search_by_tag.ts b/src/server/api/endpoints/notes/search_by_tag.ts index 317a0726d3..f982dc01e9 100644 --- a/src/server/api/endpoints/notes/search_by_tag.ts +++ b/src/server/api/endpoints/notes/search_by_tag.ts @@ -45,9 +45,9 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => const [renote = null, renoteErr] = $.bool.optional.nullable.get(params.renote); if (renoteErr) return rej('invalid renote param'); - // Get 'media' parameter - const [media = null, mediaErr] = $.bool.optional.nullable.get(params.media); - if (mediaErr) return rej('invalid media param'); + // Get 'withFiles' parameter + const [withFiles = null, withFilesErr] = $.bool.optional.nullable.get(params.withFiles); + if (withFilesErr) return rej('invalid withFiles param'); // Get 'poll' parameter const [poll = null, pollErr] = $.bool.optional.nullable.get(params.poll); @@ -244,8 +244,8 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => } } - if (media != null) { - if (media) { + if (withFiles != null) { + if (withFiles) { push({ fileIds: { $exists: true, -- cgit v1.2.3-freya From b5ff2abdb9ee0c086c8970c738cce5d61761f8f5 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 5 Sep 2018 23:55:51 +0900 Subject: 互換性のためのコードを追加 & #2623 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/server/api/endpoints/notes.ts | 128 +++++++------- src/server/api/endpoints/notes/global-timeline.ts | 77 +++++---- src/server/api/endpoints/notes/hybrid-timeline.ts | 12 +- src/server/api/endpoints/notes/local-timeline.ts | 77 +++++---- src/server/api/endpoints/notes/search_by_tag.ts | 185 ++++++++++++--------- src/server/api/endpoints/notes/timeline.ts | 12 +- .../api/endpoints/notes/user-list-timeline.ts | 12 +- src/server/api/endpoints/users/notes.ts | 178 +++++++++++++------- 8 files changed, 416 insertions(+), 265 deletions(-) (limited to 'src/server/api') diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts index 4d15e9483f..5fa58d19de 100644 --- a/src/server/api/endpoints/notes.ts +++ b/src/server/api/endpoints/notes.ts @@ -1,51 +1,65 @@ -/** - * Module dependencies - */ import $ from 'cafy'; import ID from '../../../misc/cafy-id'; import Note, { pack } from '../../../models/note'; +import getParams from '../get-params'; + +export const meta = { + desc: { + 'ja-JP': '投稿を取得します。' + }, + + params: { + local: $.bool.optional.note({ + desc: { + 'ja-JP': 'ローカルの投稿に限定するか否か' + } + }), + + reply: $.bool.optional.note({ + desc: { + 'ja-JP': '返信に限定するか否か' + } + }), + + renote: $.bool.optional.note({ + desc: { + 'ja-JP': 'Renoteに限定するか否か' + } + }), + + withFiles: $.bool.optional.note({ + desc: { + 'ja-JP': 'ファイルが添付された投稿に限定するか否か' + } + }), + + media: $.bool.optional.note({ + desc: { + 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' + } + }), + + poll: $.bool.optional.note({ + desc: { + 'ja-JP': 'アンケートが添付された投稿に限定するか否か' + } + }), + + limit: $.num.optional.range(1, 100).note({ + default: 10 + }), + + sinceId: $.type(ID).optional.note({}), + + untilId: $.type(ID).optional.note({}), + } +}; -/** - * Get all notes - */ export default (params: any) => new Promise(async (res, rej) => { - // Get 'local' parameter - const [local, localErr] = $.bool.optional.get(params.local); - if (localErr) return rej('invalid local param'); - - // Get 'reply' parameter - const [reply, replyErr] = $.bool.optional.get(params.reply); - if (replyErr) return rej('invalid reply param'); - - // Get 'renote' parameter - const [renote, renoteErr] = $.bool.optional.get(params.renote); - if (renoteErr) return rej('invalid renote param'); - - // Get 'files' parameter - const [files, filesErr] = $.bool.optional.get(params.files); - if (filesErr) return rej('invalid files param'); - - // Get 'poll' parameter - const [poll, pollErr] = $.bool.optional.get(params.poll); - if (pollErr) return rej('invalid poll param'); - - // Get 'bot' parameter - //const [bot, botErr] = $.bool.optional.get(params.bot); - //if (botErr) return rej('invalid bot param'); - - // Get 'limit' parameter - const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit); - if (limitErr) return rej('invalid limit param'); - - // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId); - if (sinceIdErr) return rej('invalid sinceId param'); - - // Get 'untilId' parameter - const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId); - if (untilIdErr) return rej('invalid untilId param'); + const [ps, psErr] = getParams(meta, params); + if (psErr) throw psErr; // Check if both of sinceId and untilId is specified - if (sinceId && untilId) { + if (ps.sinceId && ps.untilId) { return rej('cannot set sinceId and untilId'); } @@ -56,35 +70,37 @@ export default (params: any) => new Promise(async (res, rej) => { const query = { visibility: 'public' } as any; - if (sinceId) { + if (ps.sinceId) { sort._id = 1; query._id = { - $gt: sinceId + $gt: ps.sinceId }; - } else if (untilId) { + } else if (ps.untilId) { query._id = { - $lt: untilId + $lt: ps.untilId }; } - if (local) { + if (ps.local) { query['_user.host'] = null; } - if (reply != undefined) { - query.replyId = reply ? { $exists: true, $ne: null } : null; + if (ps.reply != undefined) { + query.replyId = ps.reply ? { $exists: true, $ne: null } : null; } - if (renote != undefined) { - query.renoteId = renote ? { $exists: true, $ne: null } : null; + if (ps.renote != undefined) { + query.renoteId = ps.renote ? { $exists: true, $ne: null } : null; } - if (files != undefined) { - query.fileIds = files ? { $exists: true, $ne: null } : []; + const withFiles = ps.withFiles != undefined ? ps.withFiles : ps.media; + + if (withFiles) { + query.fileIds = withFiles ? { $exists: true, $ne: null } : []; } - if (poll != undefined) { - query.poll = poll ? { $exists: true, $ne: null } : null; + if (ps.poll != undefined) { + query.poll = ps.poll ? { $exists: true, $ne: null } : null; } // TODO @@ -95,7 +111,7 @@ export default (params: any) => new Promise(async (res, rej) => { // Issue query const notes = await Note .find(query, { - limit: limit, + limit: ps.limit, sort: sort }); diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts index 554245a0f4..e70fc5d76f 100644 --- a/src/server/api/endpoints/notes/global-timeline.ts +++ b/src/server/api/endpoints/notes/global-timeline.ts @@ -3,40 +3,49 @@ import Note from '../../../../models/note'; import Mute from '../../../../models/mute'; import { pack } from '../../../../models/note'; import { ILocalUser } from '../../../../models/user'; +import getParams from '../../get-params'; -/** - * Get timeline of global - */ -export default async (params: any, user: ILocalUser) => { - // Get 'limit' parameter - const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit); - if (limitErr) throw 'invalid limit param'; +export const meta = { + desc: { + 'ja-JP': 'グローバルタイムラインを取得します。' + }, + + params: { + withFiles: $.bool.optional.note({ + desc: { + 'ja-JP': 'ファイルが添付された投稿に限定するか否か' + } + }), + + mediaOnly: $.bool.optional.note({ + desc: { + 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' + } + }), + + limit: $.num.optional.range(1, 100).note({ + default: 10 + }), + + sinceId: $.type(ID).optional.note({}), - // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId); - if (sinceIdErr) throw 'invalid sinceId param'; + untilId: $.type(ID).optional.note({}), - // Get 'untilId' parameter - const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId); - if (untilIdErr) throw 'invalid untilId param'; + sinceDate: $.num.optional.note({}), - // Get 'sinceDate' parameter - const [sinceDate, sinceDateErr] = $.num.optional.get(params.sinceDate); - if (sinceDateErr) throw 'invalid sinceDate param'; + untilDate: $.num.optional.note({}), + } +}; - // Get 'untilDate' parameter - const [untilDate, untilDateErr] = $.num.optional.get(params.untilDate); - if (untilDateErr) throw 'invalid untilDate param'; +export default async (params: any, user: ILocalUser) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) throw psErr; // Check if only one of sinceId, untilId, sinceDate, untilDate specified - if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) { + if ([ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate].filter(x => x != null).length > 1) { throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; } - // Get 'withFiles' parameter - const [withFiles, withFilesErr] = $.bool.optional.get(params.withFiles); - if (withFilesErr) throw 'invalid withFiles param'; - // ミュートしているユーザーを取得 const mutedUserIds = user ? (await Mute.find({ muterId: user._id @@ -68,27 +77,29 @@ export default async (params: any, user: ILocalUser) => { }; } + const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly; + if (withFiles) { query.fileIds = { $exists: true, $ne: [] }; } - if (sinceId) { + if (ps.sinceId) { sort._id = 1; query._id = { - $gt: sinceId + $gt: ps.sinceId }; - } else if (untilId) { + } else if (ps.untilId) { query._id = { - $lt: untilId + $lt: ps.untilId }; - } else if (sinceDate) { + } else if (ps.sinceDate) { sort._id = 1; query.createdAt = { - $gt: new Date(sinceDate) + $gt: new Date(ps.sinceDate) }; - } else if (untilDate) { + } else if (ps.untilDate) { query.createdAt = { - $lt: new Date(untilDate) + $lt: new Date(ps.untilDate) }; } //#endregion @@ -96,7 +107,7 @@ export default async (params: any, user: ILocalUser) => { // Issue query const timeline = await Note .find(query, { - limit: limit, + limit: ps.limit, sort: sort }); diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts index 1060792683..16cec86797 100644 --- a/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -7,8 +7,6 @@ import { ILocalUser } from '../../../../models/user'; import getParams from '../../get-params'; export const meta = { - name: 'notes/hybrid-timeline', - desc: { 'ja-JP': 'ハイブリッドタイムラインを取得します。' }, @@ -68,7 +66,13 @@ export const meta = { withFiles: $.bool.optional.note({ desc: { - 'ja-JP': 'true にすると、メディアが添付された投稿だけ取得します' + 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します' + } + }), + + mediaOnly: $.bool.optional.note({ + desc: { + 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' } }), } @@ -203,7 +207,7 @@ export default async (params: any, user: ILocalUser) => { }); } - if (ps.withFiles) { + if (ps.withFiles || ps.mediaOnly) { query.$and.push({ fileIds: { $exists: true, $ne: [] } }); diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts index 018e636ab5..2458a70556 100644 --- a/src/server/api/endpoints/notes/local-timeline.ts +++ b/src/server/api/endpoints/notes/local-timeline.ts @@ -3,40 +3,49 @@ import Note from '../../../../models/note'; import Mute from '../../../../models/mute'; import { pack } from '../../../../models/note'; import { ILocalUser } from '../../../../models/user'; +import getParams from '../../get-params'; -/** - * Get timeline of local - */ -export default async (params: any, user: ILocalUser) => { - // Get 'limit' parameter - const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit); - if (limitErr) throw 'invalid limit param'; +export const meta = { + desc: { + 'ja-JP': 'ローカルタイムラインを取得します。' + }, + + params: { + withFiles: $.bool.optional.note({ + desc: { + 'ja-JP': 'ファイルが添付された投稿に限定するか否か' + } + }), + + mediaOnly: $.bool.optional.note({ + desc: { + 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' + } + }), + + limit: $.num.optional.range(1, 100).note({ + default: 10 + }), + + sinceId: $.type(ID).optional.note({}), - // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId); - if (sinceIdErr) throw 'invalid sinceId param'; + untilId: $.type(ID).optional.note({}), - // Get 'untilId' parameter - const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId); - if (untilIdErr) throw 'invalid untilId param'; + sinceDate: $.num.optional.note({}), - // Get 'sinceDate' parameter - const [sinceDate, sinceDateErr] = $.num.optional.get(params.sinceDate); - if (sinceDateErr) throw 'invalid sinceDate param'; + untilDate: $.num.optional.note({}), + } +}; - // Get 'untilDate' parameter - const [untilDate, untilDateErr] = $.num.optional.get(params.untilDate); - if (untilDateErr) throw 'invalid untilDate param'; +export default async (params: any, user: ILocalUser) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) throw psErr; // Check if only one of sinceId, untilId, sinceDate, untilDate specified - if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) { + if ([ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate].filter(x => x != null).length > 1) { throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; } - // Get 'withFiles' parameter - const [withFiles, withFilesErr] = $.bool.optional.get(params.withFiles); - if (withFilesErr) throw 'invalid withFiles param'; - // ミュートしているユーザーを取得 const mutedUserIds = user ? (await Mute.find({ muterId: user._id @@ -69,27 +78,29 @@ export default async (params: any, user: ILocalUser) => { }; } + const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly; + if (withFiles) { query.fileIds = { $exists: true, $ne: [] }; } - if (sinceId) { + if (ps.sinceId) { sort._id = 1; query._id = { - $gt: sinceId + $gt: ps.sinceId }; - } else if (untilId) { + } else if (ps.untilId) { query._id = { - $lt: untilId + $lt: ps.untilId }; - } else if (sinceDate) { + } else if (ps.sinceDate) { sort._id = 1; query.createdAt = { - $gt: new Date(sinceDate) + $gt: new Date(ps.sinceDate) }; - } else if (untilDate) { + } else if (ps.untilDate) { query.createdAt = { - $lt: new Date(untilDate) + $lt: new Date(ps.untilDate) }; } //#endregion @@ -97,7 +108,7 @@ export default async (params: any, user: ILocalUser) => { // Issue query const timeline = await Note .find(query, { - limit: limit, + limit: ps.limit, sort: sort }); diff --git a/src/server/api/endpoints/notes/search_by_tag.ts b/src/server/api/endpoints/notes/search_by_tag.ts index f982dc01e9..82f11a9775 100644 --- a/src/server/api/endpoints/notes/search_by_tag.ts +++ b/src/server/api/endpoints/notes/search_by_tag.ts @@ -4,119 +4,152 @@ import User, { ILocalUser } from '../../../../models/user'; import Mute from '../../../../models/mute'; import { getFriendIds } from '../../common/get-friends'; import { pack } from '../../../../models/note'; +import getParams from '../../get-params'; -/** - * Search notes by tag - */ -export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => { - // Get 'tag' parameter - const [tag, tagError] = $.str.get(params.tag); - if (tagError) return rej('invalid tag param'); +export const meta = { + desc: { + 'ja-JP': '指定されたタグが付けられた投稿を取得します。' + }, + + params: { + tag: $.str.note({ + desc: { + 'ja-JP': 'タグ' + } + }), + + includeUserIds: $.arr($.type(ID)).optional.note({ + default: [] + }), + + excludeUserIds: $.arr($.type(ID)).optional.note({ + default: [] + }), + + includeUserUsernames: $.arr($.str).optional.note({ + default: [] + }), + + excludeUserUsernames: $.arr($.str).optional.note({ + default: [] + }), + + following: $.bool.optional.nullable.note({ + default: null + }), + + mute: $.str.optional.note({ + default: 'mute_all' + }), - // Get 'includeUserIds' parameter - const [includeUserIds = [], includeUserIdsErr] = $.arr($.type(ID)).optional.get(params.includeUserIds); - if (includeUserIdsErr) return rej('invalid includeUserIds param'); + reply: $.bool.optional.nullable.note({ + default: null, - // Get 'excludeUserIds' parameter - const [excludeUserIds = [], excludeUserIdsErr] = $.arr($.type(ID)).optional.get(params.excludeUserIds); - if (excludeUserIdsErr) return rej('invalid excludeUserIds param'); + desc: { + 'ja-JP': '返信に限定するか否か' + } + }), - // Get 'includeUserUsernames' parameter - const [includeUserUsernames = [], includeUserUsernamesErr] = $.arr($.str).optional.get(params.includeUserUsernames); - if (includeUserUsernamesErr) return rej('invalid includeUserUsernames param'); + renote: $.bool.optional.nullable.note({ + default: null, - // Get 'excludeUserUsernames' parameter - const [excludeUserUsernames = [], excludeUserUsernamesErr] = $.arr($.str).optional.get(params.excludeUserUsernames); - if (excludeUserUsernamesErr) return rej('invalid excludeUserUsernames param'); + desc: { + 'ja-JP': 'Renoteに限定するか否か' + } + }), - // Get 'following' parameter - const [following = null, followingErr] = $.bool.optional.nullable.get(params.following); - if (followingErr) return rej('invalid following param'); + withFiles: $.bool.optional.nullable.note({ + default: null, - // Get 'mute' parameter - const [mute = 'mute_all', muteErr] = $.str.optional.get(params.mute); - if (muteErr) return rej('invalid mute param'); + desc: { + 'ja-JP': 'ファイルが添付された投稿に限定するか否か' + } + }), - // Get 'reply' parameter - const [reply = null, replyErr] = $.bool.optional.nullable.get(params.reply); - if (replyErr) return rej('invalid reply param'); + media: $.bool.optional.nullable.note({ + default: null, - // Get 'renote' parameter - const [renote = null, renoteErr] = $.bool.optional.nullable.get(params.renote); - if (renoteErr) return rej('invalid renote param'); + desc: { + 'ja-JP': 'ファイルが添付された投稿に限定するか否か (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' + } + }), - // Get 'withFiles' parameter - const [withFiles = null, withFilesErr] = $.bool.optional.nullable.get(params.withFiles); - if (withFilesErr) return rej('invalid withFiles param'); + poll: $.bool.optional.nullable.note({ + default: null, - // Get 'poll' parameter - const [poll = null, pollErr] = $.bool.optional.nullable.get(params.poll); - if (pollErr) return rej('invalid poll param'); + desc: { + 'ja-JP': 'アンケートが添付された投稿に限定するか否か' + } + }), - // Get 'sinceDate' parameter - const [sinceDate, sinceDateErr] = $.num.optional.get(params.sinceDate); - if (sinceDateErr) throw 'invalid sinceDate param'; + sinceDate: $.num.optional.note({ + }), - // Get 'untilDate' parameter - const [untilDate, untilDateErr] = $.num.optional.get(params.untilDate); - if (untilDateErr) throw 'invalid untilDate param'; + untilDate: $.num.optional.note({ + }), - // Get 'offset' parameter - const [offset = 0, offsetErr] = $.num.optional.min(0).get(params.offset); - if (offsetErr) return rej('invalid offset param'); + offset: $.num.optional.min(0).note({ + default: 0 + }), - // Get 'limit' parameter - const [limit = 10, limitErr] = $.num.optional.range(1, 30).get(params.limit); - if (limitErr) return rej('invalid limit param'); + limit: $.num.optional.range(1, 30).note({ + default: 10 + }), + } +}; + +export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) throw psErr; - if (includeUserUsernames != null) { - const ids = (await Promise.all(includeUserUsernames.map(async (username) => { + if (ps.includeUserUsernames != null) { + const ids = (await Promise.all(ps.includeUserUsernames.map(async (username) => { const _user = await User.findOne({ usernameLower: username.toLowerCase() }); return _user ? _user._id : null; }))).filter(id => id != null); - ids.forEach(id => includeUserIds.push(id)); + ids.forEach(id => ps.includeUserIds.push(id)); } - if (excludeUserUsernames != null) { - const ids = (await Promise.all(excludeUserUsernames.map(async (username) => { + if (ps.excludeUserUsernames != null) { + const ids = (await Promise.all(ps.excludeUserUsernames.map(async (username) => { const _user = await User.findOne({ usernameLower: username.toLowerCase() }); return _user ? _user._id : null; }))).filter(id => id != null); - ids.forEach(id => excludeUserIds.push(id)); + ids.forEach(id => ps.excludeUserIds.push(id)); } let q: any = { $and: [{ - tagsLower: tag.toLowerCase() + tagsLower: ps.tag.toLowerCase() }] }; const push = (x: any) => q.$and.push(x); - if (includeUserIds && includeUserIds.length != 0) { + if (ps.includeUserIds && ps.includeUserIds.length != 0) { push({ userId: { - $in: includeUserIds + $in: ps.includeUserIds } }); - } else if (excludeUserIds && excludeUserIds.length != 0) { + } else if (ps.excludeUserIds && ps.excludeUserIds.length != 0) { push({ userId: { - $nin: excludeUserIds + $nin: ps.excludeUserIds } }); } - if (following != null && me != null) { + if (ps.following != null && me != null) { const ids = await getFriendIds(me._id, false); push({ - userId: following ? { + userId: ps.following ? { $in: ids } : { $nin: ids.concat(me._id) @@ -131,7 +164,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => }); const mutedUserIds = mutes.map(m => m.muteeId); - switch (mute) { + switch (ps.mute) { case 'mute_all': push({ userId: { @@ -202,8 +235,8 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => } } - if (reply != null) { - if (reply) { + if (ps.reply != null) { + if (ps.reply) { push({ replyId: { $exists: true, @@ -223,8 +256,8 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => } } - if (renote != null) { - if (renote) { + if (ps.renote != null) { + if (ps.renote) { push({ renoteId: { $exists: true, @@ -244,6 +277,8 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => } } + const withFiles = ps.withFiles != null ? ps.withFiles : ps.media; + if (withFiles != null) { if (withFiles) { push({ @@ -265,8 +300,8 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => } } - if (poll != null) { - if (poll) { + if (ps.poll != null) { + if (ps.poll) { push({ poll: { $exists: true, @@ -286,18 +321,18 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => } } - if (sinceDate) { + if (ps.sinceDate) { push({ createdAt: { - $gt: new Date(sinceDate) + $gt: new Date(ps.sinceDate) } }); } - if (untilDate) { + if (ps.untilDate) { push({ createdAt: { - $lt: new Date(untilDate) + $lt: new Date(ps.untilDate) } }); } @@ -312,8 +347,8 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => sort: { _id: -1 }, - limit: limit, - skip: offset + limit: ps.limit, + skip: ps.offset }); // Serialize diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts index 145f648c56..089e7a182a 100644 --- a/src/server/api/endpoints/notes/timeline.ts +++ b/src/server/api/endpoints/notes/timeline.ts @@ -69,7 +69,13 @@ export const meta = { withFiles: $.bool.optional.note({ desc: { - 'ja-JP': 'true にすると、メディアが添付された投稿だけ取得します' + 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します' + } + }), + + mediaOnly: $.bool.optional.note({ + desc: { + 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' } }), } @@ -193,7 +199,9 @@ export default async (params: any, user: ILocalUser) => { }); } - if (ps.withFiles) { + const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly; + + if (withFiles) { query.$and.push({ fileIds: { $exists: true, $ne: [] } }); diff --git a/src/server/api/endpoints/notes/user-list-timeline.ts b/src/server/api/endpoints/notes/user-list-timeline.ts index e00a7de371..61192d7d3e 100644 --- a/src/server/api/endpoints/notes/user-list-timeline.ts +++ b/src/server/api/endpoints/notes/user-list-timeline.ts @@ -75,7 +75,13 @@ export const meta = { withFiles: $.bool.optional.note({ desc: { - 'ja-JP': 'true にすると、メディアが添付された投稿だけ取得します' + 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します' + } + }), + + mediaOnly: $.bool.optional.note({ + desc: { + 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' } }), } @@ -199,7 +205,9 @@ export default async (params: any, user: ILocalUser) => { }); } - if (ps.withFiles) { + const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly; + + if (withFiles) { query.$and.push({ fileIds: { $exists: true, $ne: [] } }); diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts index d894e52dba..42c31189d6 100644 --- a/src/server/api/endpoints/users/notes.ts +++ b/src/server/api/endpoints/users/notes.ts @@ -2,63 +2,121 @@ import $ from 'cafy'; import ID from '../../../../misc/cafy-id'; import getHostLower from '../../common/get-host-lower'; import Note, { pack } from '../../../../models/note'; import User, { ILocalUser } from '../../../../models/user'; +import getParams from '../../get-params'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーのタイムラインを取得します。' + }, + + params: { + userId: $.type(ID).optional.note({ + desc: { + 'ja-JP': 'ユーザーID' + } + }), + + username: $.str.optional.note({ + desc: { + 'ja-JP': 'ユーザー名' + } + }), + + host: $.str.optional.note({ + }), + + includeReplies: $.bool.optional.note({ + default: true, + + desc: { + 'ja-JP': 'リプライを含めるか否か' + } + }), + + limit: $.num.optional.range(1, 100).note({ + default: 10, + desc: { + 'ja-JP': '最大数' + } + }), + + sinceId: $.type(ID).optional.note({ + desc: { + 'ja-JP': '指定すると、この投稿を基点としてより新しい投稿を取得します' + } + }), + + untilId: $.type(ID).optional.note({ + desc: { + 'ja-JP': '指定すると、この投稿を基点としてより古い投稿を取得します' + } + }), + + sinceDate: $.num.optional.note({ + desc: { + 'ja-JP': '指定した時間を基点としてより新しい投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。' + } + }), + + untilDate: $.num.optional.note({ + desc: { + 'ja-JP': '指定した時間を基点としてより古い投稿を取得します。数値は、1970年1月1日 00:00:00 UTC から指定した日時までの経過時間をミリ秒単位で表します。' + } + }), + + includeMyRenotes: $.bool.optional.note({ + default: true, + desc: { + 'ja-JP': '自分の行ったRenoteを含めるかどうか' + } + }), + + includeRenotedMyNotes: $.bool.optional.note({ + default: true, + desc: { + 'ja-JP': 'Renoteされた自分の投稿を含めるかどうか' + } + }), + + includeLocalRenotes: $.bool.optional.note({ + default: true, + desc: { + 'ja-JP': 'Renoteされたローカルの投稿を含めるかどうか' + } + }), + + withFiles: $.bool.optional.note({ + default: false, + desc: { + 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します' + } + }), + + mediaOnly: $.bool.optional.note({ + default: false, + desc: { + 'ja-JP': 'true にすると、ファイルが添付された投稿だけ取得します (このパラメータは廃止予定です。代わりに withFiles を使ってください。)' + } + }), + } +}; -/** - * Get notes of a user - */ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => { - // Get 'userId' parameter - const [userId, userIdErr] = $.type(ID).optional.get(params.userId); - if (userIdErr) return rej('invalid userId param'); - - // Get 'username' parameter - const [username, usernameErr] = $.str.optional.get(params.username); - if (usernameErr) return rej('invalid username param'); + const [ps, psErr] = getParams(meta, params); + if (psErr) throw psErr; - if (userId === undefined && username === undefined) { + if (ps.userId === undefined && ps.username === undefined) { return rej('userId or username is required'); } - // Get 'host' parameter - const [host, hostErr] = $.str.optional.get(params.host); - if (hostErr) return rej('invalid host param'); - - // Get 'includeReplies' parameter - const [includeReplies = true, includeRepliesErr] = $.bool.optional.get(params.includeReplies); - if (includeRepliesErr) return rej('invalid includeReplies param'); - - // Get 'withFiles' parameter - const [withFiles = false, withFilesErr] = $.bool.optional.get(params.withFiles); - if (withFilesErr) return rej('invalid withFiles param'); - - // Get 'limit' parameter - const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit); - if (limitErr) return rej('invalid limit param'); - - // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId); - if (sinceIdErr) return rej('invalid sinceId param'); - - // Get 'untilId' parameter - const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId); - if (untilIdErr) return rej('invalid untilId param'); - - // Get 'sinceDate' parameter - const [sinceDate, sinceDateErr] = $.num.optional.get(params.sinceDate); - if (sinceDateErr) throw 'invalid sinceDate param'; - - // Get 'untilDate' parameter - const [untilDate, untilDateErr] = $.num.optional.get(params.untilDate); - if (untilDateErr) throw 'invalid untilDate param'; - // Check if only one of sinceId, untilId, sinceDate, untilDate specified - if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) { + if ([ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate].filter(x => x != null).length > 1) { throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; } - const q = userId !== undefined - ? { _id: userId } - : { usernameLower: username.toLowerCase(), host: getHostLower(host) } ; + const q = ps.userId !== undefined + ? { _id: ps.userId } + : { usernameLower: ps.username.toLowerCase(), host: getHostLower(ps.host) } ; // Lookup user const user = await User.findOne(q, { @@ -80,30 +138,32 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => userId: user._id } as any; - if (sinceId) { + if (ps.sinceId) { sort._id = 1; query._id = { - $gt: sinceId + $gt: ps.sinceId }; - } else if (untilId) { + } else if (ps.untilId) { query._id = { - $lt: untilId + $lt: ps.untilId }; - } else if (sinceDate) { + } else if (ps.sinceDate) { sort._id = 1; query.createdAt = { - $gt: new Date(sinceDate) + $gt: new Date(ps.sinceDate) }; - } else if (untilDate) { + } else if (ps.untilDate) { query.createdAt = { - $lt: new Date(untilDate) + $lt: new Date(ps.untilDate) }; } - if (!includeReplies) { + if (!ps.includeReplies) { query.replyId = null; } + const withFiles = ps.withFiles != null ? ps.withFiles : ps.mediaOnly; + if (withFiles) { query.fileIds = { $exists: true, @@ -115,12 +175,10 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => // Issue query const notes = await Note .find(query, { - limit: limit, + limit: ps.limit, sort: sort }); // Serialize - res(await Promise.all(notes.map(async (note) => - await pack(note, me) - ))); + res(await Promise.all(notes.map(note => pack(note, me)))); }); -- cgit v1.2.3-freya From 395cfa61089efc4ab3ea1b6c6842521bad31efed Mon Sep 17 00:00:00 2001 From: Aya Morisawa Date: Thu, 6 Sep 2018 02:16:08 +0900 Subject: Resolve #2625 (#2627) --- src/client/app/desktop/views/pages/deck/deck.column.vue | 3 ++- src/games/reversi/core.ts | 6 ++++-- src/prelude/array.ts | 7 +++++++ src/server/activitypub/outbox.ts | 3 ++- src/server/api/endpoints/notes/global-timeline.ts | 3 ++- src/server/api/endpoints/notes/hybrid-timeline.ts | 3 ++- src/server/api/endpoints/notes/local-timeline.ts | 3 ++- src/server/api/endpoints/notes/timeline.ts | 3 ++- src/server/api/endpoints/users/notes.ts | 3 ++- 9 files changed, 25 insertions(+), 9 deletions(-) create mode 100644 src/prelude/array.ts (limited to 'src/server/api') diff --git a/src/client/app/desktop/views/pages/deck/deck.column.vue b/src/client/app/desktop/views/pages/deck/deck.column.vue index 239b1b0447..abb09775fb 100644 --- a/src/client/app/desktop/views/pages/deck/deck.column.vue +++ b/src/client/app/desktop/views/pages/deck/deck.column.vue @@ -28,6 +28,7 @@ import Vue from 'vue'; import Menu from '../../../../common/views/components/menu.vue'; import contextmenu from '../../../api/contextmenu'; +import { countIf } from '../../../../../../prelude/array'; export default Vue.extend({ props: { @@ -117,7 +118,7 @@ export default Vue.extend({ toggleActive() { if (!this.isStacked) return; const vms = this.$store.state.settings.deck.layout.find(ids => ids.indexOf(this.column.id) != -1).map(id => this.getColumnVm(id)); - if (this.active && vms.filter(vm => vm.$el.classList.contains('active')).length == 1) return; + if (this.active && countIf(vm => vm.$el.classList.contains('active'), vms) == 1) return; this.active = !this.active; }, diff --git a/src/games/reversi/core.ts b/src/games/reversi/core.ts index b610d46884..34eb03becb 100644 --- a/src/games/reversi/core.ts +++ b/src/games/reversi/core.ts @@ -1,3 +1,5 @@ +import { count } from "../../prelude/array"; + // MISSKEY REVERSI ENGINE /** @@ -101,14 +103,14 @@ export default class Reversi { * 黒石の数 */ public get blackCount() { - return this.board.filter(x => x === BLACK).length; + return count(BLACK, this.board); } /** * 白石の数 */ public get whiteCount() { - return this.board.filter(x => x === WHITE).length; + return count(BLACK, this.board); } /** diff --git a/src/prelude/array.ts b/src/prelude/array.ts new file mode 100644 index 0000000000..e944030a7f --- /dev/null +++ b/src/prelude/array.ts @@ -0,0 +1,7 @@ +export function countIf(f: (x: T) => boolean, xs: T[]): number { + return xs.filter(f).length; +} + +export function count(x: T, xs: T[]): number { + return countIf(y => x === y, xs); +} diff --git a/src/server/activitypub/outbox.ts b/src/server/activitypub/outbox.ts index cc7e55b5df..a5e762eea8 100644 --- a/src/server/activitypub/outbox.ts +++ b/src/server/activitypub/outbox.ts @@ -10,6 +10,7 @@ import { setResponseType } from '../activitypub'; import Note from '../../models/note'; import renderNote from '../../remote/activitypub/renderer/note'; +import { countIf } from '../../prelude/array'; export default async (ctx: Router.IRouterContext) => { const userId = new mongo.ObjectID(ctx.params.user); @@ -25,7 +26,7 @@ export default async (ctx: Router.IRouterContext) => { const page: boolean = ctx.request.query.page === 'true'; // Validate parameters - if (sinceIdErr || untilIdErr || pageErr || [sinceId, untilId].filter(x => x != null).length > 1) { + if (sinceIdErr || untilIdErr || pageErr || countIf(x => x != null, [sinceId, untilId]) > 1) { ctx.status = 400; return; } diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts index e70fc5d76f..5d93cd78ec 100644 --- a/src/server/api/endpoints/notes/global-timeline.ts +++ b/src/server/api/endpoints/notes/global-timeline.ts @@ -4,6 +4,7 @@ import Mute from '../../../../models/mute'; import { pack } from '../../../../models/note'; import { ILocalUser } from '../../../../models/user'; import getParams from '../../get-params'; +import { countIf } from '../../../../prelude/array'; export const meta = { desc: { @@ -42,7 +43,7 @@ export default async (params: any, user: ILocalUser) => { if (psErr) throw psErr; // Check if only one of sinceId, untilId, sinceDate, untilDate specified - if ([ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate].filter(x => x != null).length > 1) { + if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) { throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; } diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts index 16cec86797..0eb7b61830 100644 --- a/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -5,6 +5,7 @@ import { getFriends } from '../../common/get-friends'; import { pack } from '../../../../models/note'; import { ILocalUser } from '../../../../models/user'; import getParams from '../../get-params'; +import { countIf } from '../../../../prelude/array'; export const meta = { desc: { @@ -86,7 +87,7 @@ export default async (params: any, user: ILocalUser) => { if (psErr) throw psErr; // Check if only one of sinceId, untilId, sinceDate, untilDate specified - if ([ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate].filter(x => x != null).length > 1) { + if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) { throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; } diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts index 2458a70556..39c385853d 100644 --- a/src/server/api/endpoints/notes/local-timeline.ts +++ b/src/server/api/endpoints/notes/local-timeline.ts @@ -4,6 +4,7 @@ import Mute from '../../../../models/mute'; import { pack } from '../../../../models/note'; import { ILocalUser } from '../../../../models/user'; import getParams from '../../get-params'; +import { countIf } from '../../../../prelude/array'; export const meta = { desc: { @@ -42,7 +43,7 @@ export default async (params: any, user: ILocalUser) => { if (psErr) throw psErr; // Check if only one of sinceId, untilId, sinceDate, untilDate specified - if ([ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate].filter(x => x != null).length > 1) { + if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) { throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; } diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts index 089e7a182a..5f3844987c 100644 --- a/src/server/api/endpoints/notes/timeline.ts +++ b/src/server/api/endpoints/notes/timeline.ts @@ -5,6 +5,7 @@ import { getFriends } from '../../common/get-friends'; import { pack } from '../../../../models/note'; import { ILocalUser } from '../../../../models/user'; import getParams from '../../get-params'; +import { countIf } from '../../../../prelude/array'; export const meta = { desc: { @@ -86,7 +87,7 @@ export default async (params: any, user: ILocalUser) => { if (psErr) throw psErr; // Check if only one of sinceId, untilId, sinceDate, untilDate specified - if ([ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate].filter(x => x != null).length > 1) { + if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) { throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; } diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts index 42c31189d6..1ab7786a18 100644 --- a/src/server/api/endpoints/users/notes.ts +++ b/src/server/api/endpoints/users/notes.ts @@ -3,6 +3,7 @@ import getHostLower from '../../common/get-host-lower'; import Note, { pack } from '../../../../models/note'; import User, { ILocalUser } from '../../../../models/user'; import getParams from '../../get-params'; +import { countIf } from '../../../../prelude/array'; export const meta = { desc: { @@ -110,7 +111,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => } // Check if only one of sinceId, untilId, sinceDate, untilDate specified - if ([ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate].filter(x => x != null).length > 1) { + if (countIf(x => x != null, [ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate]) > 1) { throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; } -- cgit v1.2.3-freya From e2c6227f4713b91d94355c69d619ac2d5d865c1b Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 6 Sep 2018 04:28:22 +0900 Subject: Improve local timeline API --- src/server/api/endpoints/notes/local-timeline.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'src/server/api') diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts index 2458a70556..ce84b4135a 100644 --- a/src/server/api/endpoints/notes/local-timeline.ts +++ b/src/server/api/endpoints/notes/local-timeline.ts @@ -23,6 +23,12 @@ export const meta = { } }), + fileType: $.arr($.str).optional.note({ + desc: { + 'ja-JP': '指定された種類のファイルが添付された投稿のみを取得します' + } + }), + limit: $.num.optional.range(1, 100).note({ default: 10 }), @@ -84,6 +90,14 @@ export default async (params: any, user: ILocalUser) => { query.fileIds = { $exists: true, $ne: [] }; } + if (ps.fileType) { + query.fileIds = { $exists: true, $ne: [] }; + + query['_files.contentType'] = { + $in: ps.fileType + }; + } + if (ps.sinceId) { sort._id = 1; query._id = { -- cgit v1.2.3-freya From 8e359d54bdd05f6718d8dab28ae19e1d1f657f89 Mon Sep 17 00:00:00 2001 From: Aya Morisawa Date: Thu, 6 Sep 2018 06:06:22 +0900 Subject: if elimination (#2635) --- src/server/api/call.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'src/server/api') diff --git a/src/server/api/call.ts b/src/server/api/call.ts index e9abc11f54..ee79e0a13c 100644 --- a/src/server/api/call.ts +++ b/src/server/api/call.ts @@ -25,10 +25,8 @@ export default (endpoint: string, user: IUser, app: IApp, data: any, file?: any) return rej('YOU_ARE_NOT_ADMIN'); } - if (app && ep.meta.kind) { - if (!app.permission.some(p => p === ep.meta.kind)) { - return rej('PERMISSION_DENIED'); - } + if (app && ep.meta.kind && !app.permission.some(p => p === ep.meta.kind)) { + return rej('PERMISSION_DENIED'); } if (ep.meta.requireCredential && ep.meta.limit) { -- cgit v1.2.3-freya From 00d79487cd89f3cae675ecc0392c47ba547b69a5 Mon Sep 17 00:00:00 2001 From: Aya Morisawa Date: Fri, 7 Sep 2018 00:02:55 +0900 Subject: Add erase function (#2641) --- src/client/app/common/scripts/streaming/stream-manager.ts | 3 ++- src/client/app/common/views/components/poll-editor.vue | 3 ++- src/client/app/desktop/views/components/post-form.vue | 3 ++- src/client/app/mios.ts | 3 ++- src/client/app/mobile/views/components/post-form.vue | 3 ++- src/client/app/store.ts | 7 ++++--- src/client/app/sw.js | 4 ++-- src/prelude/array.ts | 4 ++++ src/server/api/endpoints/hashtags/trend.ts | 4 ++-- src/server/api/endpoints/notes/search_by_tag.ts | 9 +++++---- src/services/note/create.ts | 7 ++++--- 11 files changed, 31 insertions(+), 19 deletions(-) (limited to 'src/server/api') diff --git a/src/client/app/common/scripts/streaming/stream-manager.ts b/src/client/app/common/scripts/streaming/stream-manager.ts index 568b8b0372..8dd06f67d3 100644 --- a/src/client/app/common/scripts/streaming/stream-manager.ts +++ b/src/client/app/common/scripts/streaming/stream-manager.ts @@ -1,6 +1,7 @@ import { EventEmitter } from 'eventemitter3'; import * as uuid from 'uuid'; import Connection from './stream'; +import { erase } from '../../../../../prelude/array'; /** * ストリーム接続を管理するクラス @@ -89,7 +90,7 @@ export default abstract class StreamManager extends EventE * @param userId use で発行したユーザーID */ public dispose(userId) { - this.users = this.users.filter(id => id != userId); + this.users = erase(userId, this.users); this._connection.user = `Managed (${ this.users.length })`; diff --git a/src/client/app/common/views/components/poll-editor.vue b/src/client/app/common/views/components/poll-editor.vue index 115c934c8b..30d9799fec 100644 --- a/src/client/app/common/views/components/poll-editor.vue +++ b/src/client/app/common/views/components/poll-editor.vue @@ -20,6 +20,7 @@ + + diff --git a/src/client/app/desktop/views/pages/admin/admin.vue b/src/client/app/desktop/views/pages/admin/admin.vue index a71059c378..510252b447 100644 --- a/src/client/app/desktop/views/pages/admin/admin.vue +++ b/src/client/app/desktop/views/pages/admin/admin.vue @@ -5,6 +5,8 @@
  • %fa:chalkboard .fw%%i18n:@dashboard%
  • %fa:users .fw%%i18n:@users%
  • %fa:broadcast-tower .fw%%i18n:@announcements%
  • +
  • %fa:hashtag .fw%%i18n:@hashtags%
  • + @@ -17,6 +19,9 @@
    +
    + +
    @@ -33,6 +38,7 @@ import Vue from "vue"; import XDashboard from "./admin.dashboard.vue"; import XAnnouncements from "./admin.announcements.vue"; +import XHashtags from "./admin.hashtags.vue"; import XSuspendUser from "./admin.suspend-user.vue"; import XUnsuspendUser from "./admin.unsuspend-user.vue"; import XVerifyUser from "./admin.verify-user.vue"; @@ -43,6 +49,7 @@ export default Vue.extend({ components: { XDashboard, XAnnouncements, + XHashtags, XSuspendUser, XUnsuspendUser, XVerifyUser, diff --git a/src/models/meta.ts b/src/models/meta.ts index aef0163dfe..4f1977f3b5 100644 --- a/src/models/meta.ts +++ b/src/models/meta.ts @@ -4,12 +4,13 @@ const Meta = db.get('meta'); export default Meta; export type IMeta = { - broadcasts: any[]; - stats: { + broadcasts?: any[]; + stats?: { notesCount: number; originalNotesCount: number; usersCount: number; originalUsersCount: number; }; - disableRegistration: boolean; + disableRegistration?: boolean; + hidedTags?: string[]; }; diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts index 10ca15d329..f903628774 100644 --- a/src/server/api/endpoints/admin/update-meta.ts +++ b/src/server/api/endpoints/admin/update-meta.ts @@ -21,7 +21,13 @@ export const meta = { desc: { 'ja-JP': '招待制か否か' } - }) + }), + + hidedTags: $.arr($.str).optional.nullable.note({ + desc: { + 'ja-JP': '統計などで無視するハッシュタグ' + } + }), } }; @@ -39,6 +45,10 @@ export default (params: any) => new Promise(async (res, rej) => { set.disableRegistration = ps.disableRegistration; } + if (Array.isArray(ps.hidedTags)) { + set.hidedTags = ps.hidedTags; + } + await Meta.update({}, { $set: set }, { upsert: true }); diff --git a/src/server/api/endpoints/hashtags/trend.ts b/src/server/api/endpoints/hashtags/trend.ts index e7c08ca9f0..bfa475619c 100644 --- a/src/server/api/endpoints/hashtags/trend.ts +++ b/src/server/api/endpoints/hashtags/trend.ts @@ -1,5 +1,6 @@ import Note from '../../../../models/note'; import { erase } from '../../../../prelude/array'; +import Meta from '../../../../models/meta'; /* トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要 @@ -17,6 +18,9 @@ const max = 5; * Get trends of hashtags */ export default () => new Promise(async (res, rej) => { + const meta = await Meta.findOne({}); + const hidedTags = (meta.hidedTags || []).map(t => t.toLowerCase()); + //#region 1. 直近Aの内に投稿されたハッシュタグ(とユーザーのペア)を集計 const data = await Note.aggregate([{ $match: { @@ -53,6 +57,9 @@ export default () => new Promise(async (res, rej) => { // カウント data.map(x => x._id).forEach(x => { + // ブラックリストに登録されているタグなら弾く + if (hidedTags.includes(x.tag)) return; + const i = tags.findIndex(tag => tag.name == x.tag); if (i != -1) { tags[i].count++; diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts index b0876eaafd..4472d8d779 100644 --- a/src/server/api/endpoints/meta.ts +++ b/src/server/api/endpoints/meta.ts @@ -4,6 +4,7 @@ import * as os from 'os'; import config from '../../../config'; import Meta from '../../../models/meta'; +import { ILocalUser } from '../../../models/user'; const pkg = require('../../../../package.json'); const client = require('../../../../built/client/meta.json'); @@ -11,7 +12,7 @@ const client = require('../../../../built/client/meta.json'); /** * Show core info */ -export default () => new Promise(async (res, rej) => { +export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => { const meta: any = (await Meta.findOne()) || {}; res({ @@ -35,6 +36,7 @@ export default () => new Promise(async (res, rej) => { disableRegistration: meta.disableRegistration, driveCapacityPerLocalUserMb: config.localDriveCapacityMb, recaptchaSitekey: config.recaptcha ? config.recaptcha.site_key : null, - swPublickey: config.sw ? config.sw.public_key : null + swPublickey: config.sw ? config.sw.public_key : null, + hidedTags: (me && me.isAdmin) ? meta.hidedTags : undefined }); }); -- cgit v1.2.3-freya From e0deaec695650d22c92512cc2672ba3aade96eed Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 7 Sep 2018 19:23:39 +0900 Subject: Implement new endpoint --- src/server/api/endpoints/aggregation/hashtags.ts | 64 ++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/server/api/endpoints/aggregation/hashtags.ts (limited to 'src/server/api') diff --git a/src/server/api/endpoints/aggregation/hashtags.ts b/src/server/api/endpoints/aggregation/hashtags.ts new file mode 100644 index 0000000000..0daf42427b --- /dev/null +++ b/src/server/api/endpoints/aggregation/hashtags.ts @@ -0,0 +1,64 @@ +import Note from '../../../../models/note'; +import Meta from '../../../../models/meta'; + +export default () => new Promise(async (res, rej) => { + const meta = await Meta.findOne({}); + const hidedTags = (meta.hidedTags || []).map(t => t.toLowerCase()); + + const span = 1000 * 60 * 60 * 24 * 7; // 1週間 + + //#region 1. 指定期間の内に投稿されたハッシュタグ(とユーザーのペア)を集計 + const data = await Note.aggregate([{ + $match: { + createdAt: { + $gt: new Date(Date.now() - span) + }, + tagsLower: { + $exists: true, + $ne: [] + } + } + }, { + $unwind: '$tagsLower' + }, { + $group: { + _id: { tag: '$tagsLower', userId: '$userId' } + } + }]) as Array<{ + _id: { + tag: string; + userId: any; + } + }>; + //#endregion + + if (data.length == 0) { + return res([]); + } + + let tags: Array<{ + name: string; + count: number; + }> = []; + + // カウント + data.map(x => x._id).forEach(x => { + // ブラックリストに登録されているタグなら弾く + if (hidedTags.includes(x.tag)) return; + + const i = tags.findIndex(tag => tag.name == x.tag); + if (i != -1) { + tags[i].count++; + } else { + tags.push({ + name: x.tag, + count: 1 + }); + } + }); + + // タグを人気順に並べ替え + tags = tags.sort((a, b) => b.count - a.count); + + res(tags); +}); -- cgit v1.2.3-freya From d9fe9cc5df7d3b7964a303544dd3dbbdf1cf5dd7 Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 7 Sep 2018 20:23:46 +0900 Subject: 返すタグの数を制限 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/server/api/endpoints/aggregation/hashtags.ts | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/server/api') diff --git a/src/server/api/endpoints/aggregation/hashtags.ts b/src/server/api/endpoints/aggregation/hashtags.ts index 0daf42427b..c5aacd89cd 100644 --- a/src/server/api/endpoints/aggregation/hashtags.ts +++ b/src/server/api/endpoints/aggregation/hashtags.ts @@ -60,5 +60,7 @@ export default () => new Promise(async (res, rej) => { // タグを人気順に並べ替え tags = tags.sort((a, b) => b.count - a.count); + tags = tags.slice(0, 30); + res(tags); }); -- cgit v1.2.3-freya From fd06fd4dc18313100db3f06e771c3c4258f24176 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 10 Sep 2018 01:51:46 +0900 Subject: Resolve #2560 --- src/server/api/endpoints/notes/create.ts | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) (limited to 'src/server/api') diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts index 47b53c943b..96745132a3 100644 --- a/src/server/api/endpoints/notes/create.ts +++ b/src/server/api/endpoints/notes/create.ts @@ -132,25 +132,14 @@ export default (params: any, user: ILocalUser, app: IApp) => new Promise(async ( let files: IDriveFile[] = []; const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null; if (fileIds != null) { - // Fetch files - // forEach だと途中でエラーなどがあっても return できないので - // 敢えて for を使っています。 - for (const fileId of fileIds) { - // Fetch file - // SELECT _id - const entity = await DriveFile.findOne({ + files = await Promise.all(fileIds.map(fileId => { + return DriveFile.findOne({ _id: fileId, 'metadata.userId': user._id }); + })); - if (entity === null) { - return rej('file not found'); - } else { - files.push(entity); - } - } - } else { - files = null; + files = files.filter(file => file != null); } let renote: INote = null; -- cgit v1.2.3-freya From 1344ffa67dbe74505bd922ac7185a4d41dfdddd7 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 10 Sep 2018 01:55:14 +0900 Subject: 削除された投稿にリアクションできないように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1778 --- src/server/api/endpoints/notes/reactions/create.ts | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src/server/api') diff --git a/src/server/api/endpoints/notes/reactions/create.ts b/src/server/api/endpoints/notes/reactions/create.ts index 0781db16c5..ec68f065d8 100644 --- a/src/server/api/endpoints/notes/reactions/create.ts +++ b/src/server/api/endpoints/notes/reactions/create.ts @@ -43,6 +43,10 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = return rej('note not found'); } + if (note.deletedAt != null) { + return rej('this not is already deleted'); + } + try { await create(user, note, ps.reaction); } catch (e) { -- cgit v1.2.3-freya From e32884f07f0b05028a581c73073fa87da08d9fd0 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 10 Sep 2018 01:57:19 +0900 Subject: Fix #1740 --- src/server/api/endpoints/notes/search_by_tag.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src/server/api') diff --git a/src/server/api/endpoints/notes/search_by_tag.ts b/src/server/api/endpoints/notes/search_by_tag.ts index 77082c2600..fadebe4efe 100644 --- a/src/server/api/endpoints/notes/search_by_tag.ts +++ b/src/server/api/endpoints/notes/search_by_tag.ts @@ -128,7 +128,8 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => let q: any = { $and: [{ tagsLower: ps.tag.toLowerCase() - }] + }], + deletedAt: { $exists: false } }; const push = (x: any) => q.$and.push(x); @@ -339,7 +340,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => } if (q.$and.length == 0) { - q = {}; + delete q.$and; } // Search notes -- cgit v1.2.3-freya From eb4f625bbdadd63a32b9d6f09714b721e510defe Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 10 Sep 2018 02:09:33 +0900 Subject: Fix #2096 --- src/server/api/endpoints/i/update.ts | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/server/api') diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index 585339e249..953a6aec2a 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -84,6 +84,7 @@ export default async (params: any, user: ILocalUser, app: IApp) => new Promise(a }); if (avatar == null) return rej('avatar not found'); + if (!avatar.contentType.startsWith('image/')) return rej('avatar not an image'); updates.avatarUrl = avatar.metadata.thumbnailUrl || avatar.metadata.url || `${config.drive_url}/${avatar._id}`; @@ -98,6 +99,7 @@ export default async (params: any, user: ILocalUser, app: IApp) => new Promise(a }); if (banner == null) return rej('banner not found'); + if (!banner.contentType.startsWith('image/')) return rej('banner not an image'); updates.bannerUrl = banner.metadata.url || `${config.drive_url}/${banner._id}`; -- cgit v1.2.3-freya From a32071541a0f76c25a8246e1d324f912a6843cfc Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 10 Sep 2018 02:20:09 +0900 Subject: Clean up --- src/server/api/endpoints/notes/hybrid-timeline.ts | 3 --- 1 file changed, 3 deletions(-) (limited to 'src/server/api') diff --git a/src/server/api/endpoints/notes/hybrid-timeline.ts b/src/server/api/endpoints/notes/hybrid-timeline.ts index 0eb7b61830..5e39d8c78a 100644 --- a/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -79,9 +79,6 @@ export const meta = { } }; -/** - * Get hybrid timeline of myself - */ export default async (params: any, user: ILocalUser) => { const [ps, psErr] = getParams(meta, params); if (psErr) throw psErr; -- cgit v1.2.3-freya From d9a1cd082c53e27fe984157b99c26d8e623aeee7 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 10 Sep 2018 02:21:16 +0900 Subject: #2623 --- src/server/api/endpoints/i/update.ts | 157 +++++++++++++++++++++-------------- 1 file changed, 93 insertions(+), 64 deletions(-) (limited to 'src/server/api') diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index 953a6aec2a..6aa4cc1148 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -6,6 +6,7 @@ import acceptAllFollowRequests from '../../../../services/following/requests/acc import { IApp } from '../../../../models/app'; import config from '../../../../config'; import { publishToFollowers } from '../../../../services/i/update'; +import getParams from '../../get-params'; export const meta = { desc: { @@ -15,72 +16,100 @@ export const meta = { requireCredential: true, - kind: 'account-write' + kind: 'account-write', + + params: { + name: $.str.optional.nullable.pipe(isValidName).note({ + desc: { + 'ja-JP': '名前(ハンドルネームやニックネーム)' + } + }), + + description: $.str.optional.nullable.pipe(isValidDescription).note({ + desc: { + 'ja-JP': 'アカウントの説明や自己紹介' + } + }), + + location: $.str.optional.nullable.pipe(isValidLocation).note({ + desc: { + 'ja-JP': '住んでいる地域、所在' + } + }), + + birthday: $.str.optional.nullable.pipe(isValidBirthday).note({ + desc: { + 'ja-JP': '誕生日 (YYYY-MM-DD形式)' + } + }), + + avatarId: $.type(ID).optional.nullable.note({ + desc: { + 'ja-JP': 'アイコンに設定する画像のドライブファイルID' + } + }), + + bannerId: $.type(ID).optional.nullable.note({ + desc: { + 'ja-JP': 'バナーに設定する画像のドライブファイルID' + } + }), + + wallpaperId: $.type(ID).optional.nullable.note({ + desc: { + 'ja-JP': '壁紙に設定する画像のドライブファイルID' + } + }), + + isLocked: $.bool.optional.note({ + desc: { + 'ja-JP': '鍵アカウントか否か' + } + }), + + isBot: $.bool.optional.note({ + desc: { + 'ja-JP': 'Botか否か' + } + }), + + isCat: $.bool.optional.note({ + desc: { + 'ja-JP': '猫か否か' + } + }), + + autoWatch: $.bool.optional.note({ + desc: { + 'ja-JP': '投稿の自動ウォッチをするか否か' + } + }), + } }; export default async (params: any, user: ILocalUser, app: IApp) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) throw psErr; + const isSecure = user != null && app == null; const updates = {} as any; - // Get 'name' parameter - const [name, nameErr] = $.str.optional.nullable.pipe(isValidName).get(params.name); - if (nameErr) return rej('invalid name param'); - if (name) updates.name = name; - - // Get 'description' parameter - const [description, descriptionErr] = $.str.optional.nullable.pipe(isValidDescription).get(params.description); - if (descriptionErr) return rej('invalid description param'); - if (description !== undefined) updates.description = description; - - // Get 'location' parameter - const [location, locationErr] = $.str.optional.nullable.pipe(isValidLocation).get(params.location); - if (locationErr) return rej('invalid location param'); - if (location !== undefined) updates['profile.location'] = location; - - // Get 'birthday' parameter - const [birthday, birthdayErr] = $.str.optional.nullable.pipe(isValidBirthday).get(params.birthday); - if (birthdayErr) return rej('invalid birthday param'); - if (birthday !== undefined) updates['profile.birthday'] = birthday; - - // Get 'avatarId' parameter - const [avatarId, avatarIdErr] = $.type(ID).optional.nullable.get(params.avatarId); - if (avatarIdErr) return rej('invalid avatarId param'); - if (avatarId !== undefined) updates.avatarId = avatarId; - - // Get 'bannerId' parameter - const [bannerId, bannerIdErr] = $.type(ID).optional.nullable.get(params.bannerId); - if (bannerIdErr) return rej('invalid bannerId param'); - if (bannerId !== undefined) updates.bannerId = bannerId; - - // Get 'wallpaperId' parameter - const [wallpaperId, wallpaperIdErr] = $.type(ID).optional.nullable.get(params.wallpaperId); - if (wallpaperIdErr) return rej('invalid wallpaperId param'); - if (wallpaperId !== undefined) updates.wallpaperId = wallpaperId; - - // Get 'isLocked' parameter - const [isLocked, isLockedErr] = $.bool.optional.get(params.isLocked); - if (isLockedErr) return rej('invalid isLocked param'); - if (isLocked != null) updates.isLocked = isLocked; - - // Get 'isBot' parameter - const [isBot, isBotErr] = $.bool.optional.get(params.isBot); - if (isBotErr) return rej('invalid isBot param'); - if (isBot != null) updates.isBot = isBot; - - // Get 'isCat' parameter - const [isCat, isCatErr] = $.bool.optional.get(params.isCat); - if (isCatErr) return rej('invalid isCat param'); - if (isCat != null) updates.isCat = isCat; - - // Get 'autoWatch' parameter - const [autoWatch, autoWatchErr] = $.bool.optional.get(params.autoWatch); - if (autoWatchErr) return rej('invalid autoWatch param'); - if (autoWatch != null) updates['settings.autoWatch'] = autoWatch; - - if (avatarId) { + if (ps.name !== undefined) updates.name = ps.name; + if (ps.description !== undefined) updates.description = ps.description; + if (ps.location !== undefined) updates['profile.location'] = ps.location; + if (ps.birthday !== undefined) updates['profile.birthday'] = ps.birthday; + if (ps.avatarId !== undefined) updates.avatarId = ps.avatarId; + if (ps.bannerId !== undefined) updates.bannerId = ps.bannerId; + if (ps.wallpaperId !== undefined) updates.wallpaperId = ps.wallpaperId; + if (typeof ps.isLocked == 'boolean') updates.isLocked = ps.isLocked; + if (typeof ps.isBot == 'boolean') updates.isBot = ps.isBot; + if (typeof ps.isCat == 'boolean') updates.isCat = ps.isCat; + if (typeof ps.autoWatch == 'boolean') updates['settings.autoWatch'] = ps.autoWatch; + + if (ps.avatarId) { const avatar = await DriveFile.findOne({ - _id: avatarId + _id: ps.avatarId }); if (avatar == null) return rej('avatar not found'); @@ -93,9 +122,9 @@ export default async (params: any, user: ILocalUser, app: IApp) => new Promise(a } } - if (bannerId) { + if (ps.bannerId) { const banner = await DriveFile.findOne({ - _id: bannerId + _id: ps.bannerId }); if (banner == null) return rej('banner not found'); @@ -108,13 +137,13 @@ export default async (params: any, user: ILocalUser, app: IApp) => new Promise(a } } - if (wallpaperId !== undefined) { - if (wallpaperId === null) { + if (ps.wallpaperId !== undefined) { + if (ps.wallpaperId === null) { updates.wallpaperUrl = null; updates.wallpaperColor = null; } else { const wallpaper = await DriveFile.findOne({ - _id: wallpaperId + _id: ps.wallpaperId }); if (wallpaper == null) return rej('wallpaper not found'); @@ -144,7 +173,7 @@ export default async (params: any, user: ILocalUser, app: IApp) => new Promise(a publishUserStream(user._id, 'meUpdated', iObj); // 鍵垢を解除したとき、溜まっていたフォローリクエストがあるならすべて承認 - if (user.isLocked && isLocked === false) { + if (user.isLocked && ps.isLocked === false) { acceptAllFollowRequests(user); } -- cgit v1.2.3-freya From 23efaae85ebfa722fba132004a52c2444a614808 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 10 Sep 2018 03:39:00 +0900 Subject: lint --- src/server/api/endpoints/notes/search_by_tag.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/server/api') diff --git a/src/server/api/endpoints/notes/search_by_tag.ts b/src/server/api/endpoints/notes/search_by_tag.ts index fadebe4efe..11bfe34724 100644 --- a/src/server/api/endpoints/notes/search_by_tag.ts +++ b/src/server/api/endpoints/notes/search_by_tag.ts @@ -125,7 +125,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => ids.forEach(id => ps.excludeUserIds.push(id)); } - let q: any = { + const q: any = { $and: [{ tagsLower: ps.tag.toLowerCase() }], -- cgit v1.2.3-freya From 1fea2cdcbe341bb56be1cd2b79a8115b482fab65 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 11 Sep 2018 20:57:25 +0900 Subject: Fix bug --- src/server/api/stream/home.ts | 14 +++++++------- src/server/api/stream/hybrid-timeline.ts | 14 +++++++------- src/server/api/stream/local-timeline.ts | 14 +++++++------- 3 files changed, 21 insertions(+), 21 deletions(-) (limited to 'src/server/api') diff --git a/src/server/api/stream/home.ts b/src/server/api/stream/home.ts index dc3ce9d19f..5f3b6744b2 100644 --- a/src/server/api/stream/home.ts +++ b/src/server/api/stream/home.ts @@ -36,6 +36,13 @@ export default async function( // Subscribe Home stream channel subscriber.on(`user-stream:${user._id}`, async x => { + // Renoteなら再pack + if (x.type == 'note' && x.body.renoteId != null) { + x.body.renote = await pack(x.body.renoteId, user, { + detail: true + }); + } + //#region 流れてきたメッセージがミュートしているユーザーが関わるものだったら無視する if (x.type == 'note') { if (mutedUserIds.includes(x.body.userId)) { @@ -54,13 +61,6 @@ export default async function( } //#endregion - // Renoteなら再pack - if (x.type == 'note' && x.body.renoteId != null) { - x.body.renote = await pack(x.body.renoteId, user, { - detail: true - }); - } - connection.send(JSON.stringify(x)); }); diff --git a/src/server/api/stream/hybrid-timeline.ts b/src/server/api/stream/hybrid-timeline.ts index c401145abe..d0dae9b0dd 100644 --- a/src/server/api/stream/hybrid-timeline.ts +++ b/src/server/api/stream/hybrid-timeline.ts @@ -19,6 +19,13 @@ export default async function( subscriber.on(`hybrid-timeline:${user._id}`, onEvent); async function onEvent(note: any) { + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await pack(note.renoteId, user, { + detail: true + }); + } + //#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する if (mutedUserIds.indexOf(note.userId) != -1) { return; @@ -31,13 +38,6 @@ export default async function( } //#endregion - // Renoteなら再pack - if (note.renoteId != null) { - note.renote = await pack(note.renoteId, user, { - detail: true - }); - } - connection.send(JSON.stringify({ type: 'note', body: note diff --git a/src/server/api/stream/local-timeline.ts b/src/server/api/stream/local-timeline.ts index 25e0e00c9f..e21c071bab 100644 --- a/src/server/api/stream/local-timeline.ts +++ b/src/server/api/stream/local-timeline.ts @@ -16,6 +16,13 @@ export default async function( // Subscribe stream subscriber.on('local-timeline', async note => { + // Renoteなら再pack + if (note.renoteId != null) { + note.renote = await pack(note.renoteId, user, { + detail: true + }); + } + //#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する if (mutedUserIds.indexOf(note.userId) != -1) { return; @@ -28,13 +35,6 @@ export default async function( } //#endregion - // Renoteなら再pack - if (note.renoteId != null) { - note.renote = await pack(note.renoteId, user, { - detail: true - }); - } - connection.send(JSON.stringify({ type: 'note', body: note -- cgit v1.2.3-freya From 046976dffc1aa8bc02259ab4a65e74b1216a0ec3 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 12 Sep 2018 02:48:19 +0900 Subject: Resolve #2691 --- .../app/desktop/views/components/timeline.vue | 11 +- .../desktop/views/pages/admin/admin.dashboard.vue | 35 ++++-- src/client/app/mobile/views/pages/home.vue | 11 +- src/models/meta.ts | 1 + src/server/api/endpoints/admin/update-meta.ts | 10 ++ src/server/api/endpoints/meta.ts | 1 + src/stream.ts | 119 ++++++++++++++------- 7 files changed, 134 insertions(+), 54 deletions(-) (limited to 'src/server/api') diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue index 52a7753438..8d72016f22 100644 --- a/src/client/app/desktop/views/components/timeline.vue +++ b/src/client/app/desktop/views/components/timeline.vue @@ -2,8 +2,8 @@
    %fa:home% %i18n:@home% - %fa:R comments% %i18n:@local% - %fa:share-alt% %i18n:@hybrid% + %fa:R comments% %i18n:@local% + %fa:share-alt% %i18n:@hybrid% %fa:globe% %i18n:@global% %fa:list% {{ list.title }} @@ -29,7 +29,8 @@ export default Vue.extend({ data() { return { src: 'home', - list: null + list: null, + enableLocalTimeline: false }; }, @@ -44,6 +45,10 @@ export default Vue.extend({ }, created() { + (this as any).os.getMeta().then(meta => { + this.enableLocalTimeline = !meta.disableLocalTimeline; + }); + if (this.$store.state.device.tl) { this.src = this.$store.state.device.tl.src; if (this.src == 'list') { diff --git a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue index ebb54d782e..c86c30db17 100644 --- a/src/client/app/desktop/views/pages/admin/admin.dashboard.vue +++ b/src/client/app/desktop/views/pages/admin/admin.dashboard.vue @@ -1,22 +1,34 @@ @@ -33,6 +45,7 @@ export default Vue.extend({ return { stats: null, disableRegistration: false, + disableLocalTimeline: false, inviteCode: null, connection: null, connectionId: null @@ -44,6 +57,7 @@ export default Vue.extend({ (this as any).os.getMeta().then(meta => { this.disableRegistration = meta.disableRegistration; + this.disableLocalTimeline = meta.disableLocalTimeline; }); (this as any).api('stats').then(stats => { @@ -61,7 +75,8 @@ export default Vue.extend({ }, updateMeta() { (this as any).api('admin/update-meta', { - disableRegistration: this.disableRegistration + disableRegistration: this.disableRegistration, + disableLocalTimeline: this.disableLocalTimeline }); } } @@ -97,4 +112,8 @@ export default Vue.extend({ border solid 1px #eee border-radius: 8px + > .form + > div + border-bottom solid 1px #eee + diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue index 706c9cd28b..333ca1a7a1 100644 --- a/src/client/app/mobile/views/pages/home.vue +++ b/src/client/app/mobile/views/pages/home.vue @@ -24,8 +24,8 @@
    %fa:home% %i18n:@home% - %fa:R comments% %i18n:@local% - %fa:share-alt% %i18n:@hybrid% + %fa:R comments% %i18n:@local% + %fa:share-alt% %i18n:@hybrid% %fa:globe% %i18n:@global% @@ -46,6 +47,12 @@ export default Vue.extend({ birthday: null, }; }, + computed: { + alwaysMarkNsfw: { + get() { return this.$store.state.i.settings.alwaysMarkNsfw; }, + set(value) { (this as any).api('i/update', { alwaysMarkNsfw: value }); } + }, + }, created() { this.name = this.$store.state.i.name || ''; this.location = this.$store.state.i.profile.location; diff --git a/src/client/app/mobile/views/pages/settings/settings.profile.vue b/src/client/app/mobile/views/pages/settings/settings.profile.vue index 6f5ac9ae93..127f531902 100644 --- a/src/client/app/mobile/views/pages/settings/settings.profile.vue +++ b/src/client/app/mobile/views/pages/settings/settings.profile.vue @@ -49,6 +49,7 @@
    %i18n:@is-cat% + %i18n:common.always-mark-nsfw%
    @@ -85,6 +86,13 @@ export default Vue.extend({ }; }, + computed: { + alwaysMarkNsfw: { + get() { return this.$store.state.i.settings.alwaysMarkNsfw; }, + set(value) { (this as any).api('i/update', { alwaysMarkNsfw: value }); } + }, + }, + created() { this.name = this.$store.state.i.name || ''; this.username = this.$store.state.i.username; diff --git a/src/models/user.ts b/src/models/user.ts index 8f3fbbdc8f..64197c91c2 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -102,7 +102,10 @@ export interface ILocalUser extends IUserBase { twoFactorEnabled: boolean; twoFactorTempSecret?: string; clientSettings: any; - settings: any; + settings: { + autoWatch: boolean; + alwaysMarkNsfw?: boolean; + }; hasUnreadNotification: boolean; hasUnreadMessagingMessage: boolean; } diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts index dfbd11d0c2..4b5ffa90e0 100644 --- a/src/server/api/endpoints/drive/files/create.ts +++ b/src/server/api/endpoints/drive/files/create.ts @@ -31,8 +31,8 @@ export const meta = { } }), - isSensitive: $.bool.optional.note({ - default: false, + isSensitive: $.bool.optional.nullable.note({ + default: null, desc: { 'ja-JP': 'このメディアが「閲覧注意」(NSFW)かどうか', 'en-US': 'Whether this media is NSFW' diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index 6aa4cc1148..c1be0b6ebc 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -84,6 +84,12 @@ export const meta = { 'ja-JP': '投稿の自動ウォッチをするか否か' } }), + + alwaysMarkNsfw: $.bool.optional.note({ + desc: { + 'ja-JP': 'アップロードするメディアをデフォルトで「閲覧注意」として設定するか' + } + }), } }; @@ -106,6 +112,7 @@ export default async (params: any, user: ILocalUser, app: IApp) => new Promise(a if (typeof ps.isBot == 'boolean') updates.isBot = ps.isBot; if (typeof ps.isCat == 'boolean') updates.isCat = ps.isCat; if (typeof ps.autoWatch == 'boolean') updates['settings.autoWatch'] = ps.autoWatch; + if (typeof ps.alwaysMarkNsfw == 'boolean') updates['settings.alwaysMarkNsfw'] = ps.alwaysMarkNsfw; if (ps.avatarId) { const avatar = await DriveFile.findOne({ diff --git a/src/services/drive/add-file.ts b/src/services/drive/add-file.ts index 828ebcbb94..666a6ca742 100644 --- a/src/services/drive/add-file.ts +++ b/src/services/drive/add-file.ts @@ -153,7 +153,7 @@ export default async function( isLink: boolean = false, url: string = null, uri: string = null, - sensitive = false + sensitive: boolean = null ): Promise { // Calc md5 hash const calcHash = new Promise((res, rej) => { @@ -329,7 +329,13 @@ export default async function( properties: properties, withoutChunks: isLink, isRemote: isLink, - isSensitive: sensitive + isSensitive: (sensitive !== null && sensitive !== undefined) + ? sensitive + : isLocalUser(user) + ? user.settings.alwaysMarkNsfw + ? true + : false + : false } as IMetadata; if (url !== null) { -- cgit v1.2.3-freya From c985fed3e43bae05f9e6e854f651f49f2bc3e83a Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 15 Sep 2018 05:40:58 +0900 Subject: Resolve #2328 --- locales/ja-JP.yml | 4 + package.json | 1 + src/client/app/desktop/views/components/charts.vue | 103 ++++++++++++++++++++- src/models/stats.ts | 26 ++++++ src/server/api/endpoints/chart.ts | 21 +++++ src/server/index.ts | 25 +++++ src/services/update-chart.ts | 22 +++++ 7 files changed, 200 insertions(+), 2 deletions(-) (limited to 'src/server/api') diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 5257d616c4..72dc88b7b3 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -518,6 +518,7 @@ desktop/views/components/charts.vue: notes: "投稿" users: "ユーザー" drive: "ドライブ" + network: "ネットワーク" charts: notes: "投稿の増減 (統合)" local-notes: "投稿の増減 (ローカル)" @@ -529,6 +530,9 @@ desktop/views/components/charts.vue: drive-total: "ドライブ使用量の累計" drive-files: "ドライブのファイル数の増減" drive-files-total: "ドライブのファイル数の累計" + network-requests: "リクエスト" + network-time: "応答時間" + network-usage: "通信量" desktop/views/components/choose-file-from-drive-window.vue: choose-file: "ファイル選択中" diff --git a/package.json b/package.json index 4d2a4b285b..4b9eaf9e22 100644 --- a/package.json +++ b/package.json @@ -179,6 +179,7 @@ "redis": "2.8.0", "request": "2.88.0", "request-promise-native": "1.0.5", + "request-stats": "3.0.0", "rimraf": "2.6.2", "rndstr": "1.0.0", "s-age": "1.1.2", diff --git a/src/client/app/desktop/views/components/charts.vue b/src/client/app/desktop/views/components/charts.vue index c4e92e429f..6514cdf788 100644 --- a/src/client/app/desktop/views/components/charts.vue +++ b/src/client/app/desktop/views/components/charts.vue @@ -19,6 +19,11 @@ + + + + +
    %i18n:@per-day% | %i18n:@per-hour% @@ -41,7 +46,10 @@ const colors = { localPlus: 'rgb(52, 178, 118)', remotePlus: 'rgb(158, 255, 209)', localMinus: 'rgb(255, 97, 74)', - remoteMinus: 'rgb(255, 149, 134)' + remoteMinus: 'rgb(255, 149, 134)', + + incoming: 'rgb(52, 178, 118)', + outgoing: 'rgb(255, 97, 74)', }; const rgba = (color: string): string => { @@ -75,6 +83,9 @@ export default Vue.extend({ case 'drive-total': return this.driveTotalChart(); case 'drive-files': return this.driveFilesChart(); case 'drive-files-total': return this.driveFilesTotalChart(); + case 'network-requests': return this.networkRequestsChart(); + case 'network-time': return this.networkTimeChart(); + case 'network-usage': return this.networkUsageChart(); } }, @@ -544,7 +555,95 @@ export default Vue.extend({ } } }]; - } + }, + + networkRequestsChart(): any { + const data = this.stats.slice().reverse().map(x => ({ + date: new Date(x.date), + requests: x.network.requests + })); + + return [{ + datasets: [{ + label: 'Requests', + fill: true, + backgroundColor: rgba(colors.localPlus), + borderColor: colors.localPlus, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.requests })) + }] + }]; + }, + + networkTimeChart(): any { + const data = this.stats.slice().reverse().map(x => ({ + date: new Date(x.date), + time: x.network.requests != 0 ? (x.network.totalTime / x.network.requests) : 0, + })); + + return [{ + datasets: [{ + label: 'Avg time (ms)', + fill: true, + backgroundColor: rgba(colors.localPlus), + borderColor: colors.localPlus, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.time })) + }] + }]; + }, + + networkUsageChart(): any { + const data = this.stats.slice().reverse().map(x => ({ + date: new Date(x.date), + incoming: x.network.incomingBytes, + outgoing: x.network.outgoingBytes + })); + + return [{ + datasets: [{ + label: 'Incoming', + fill: true, + backgroundColor: rgba(colors.incoming), + borderColor: colors.incoming, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.incoming })) + }, { + label: 'Outgoing', + fill: true, + backgroundColor: rgba(colors.outgoing), + borderColor: colors.outgoing, + borderWidth: 2, + pointBackgroundColor: '#fff', + lineTension: 0, + data: data.map(x => ({ t: x.date, y: x.outgoing })) + }] + }, { + scales: { + yAxes: [{ + ticks: { + callback: value => { + return Vue.filter('bytes')(value, 1); + } + } + }] + }, + tooltips: { + callbacks: { + label: (tooltipItem, data) => { + const label = data.datasets[tooltipItem.datasetIndex].label || ''; + return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`; + } + } + } + }]; + }, } }); diff --git a/src/models/stats.ts b/src/models/stats.ts index d496f2c480..c4c838caeb 100644 --- a/src/models/stats.ts +++ b/src/models/stats.ts @@ -204,4 +204,30 @@ export interface IStats { decSize: number; }; }; + + /** + * ネットワークに関する統計 + */ + network: { + /** + * サーバーへのリクエスト数 + */ + requests: number; + + /** + * 応答時間の合計 + * TIP: (totalTime / requests) でひとつのリクエストに平均でどれくらいの時間がかかったか知れる + */ + totalTime: number; + + /** + * 合計受信データ量 + */ + incomingBytes: number; + + /** + * 合計送信データ量 + */ + outgoingBytes: number; + }; } diff --git a/src/server/api/endpoints/chart.ts b/src/server/api/endpoints/chart.ts index 7da970131e..3b1a3b56fc 100644 --- a/src/server/api/endpoints/chart.ts +++ b/src/server/api/endpoints/chart.ts @@ -6,6 +6,15 @@ type Omit = Pick>; function migrateStats(stats: IStats[]) { stats.forEach(stat => { + if (stat.network == null) { + stat.network = { + requests: 0, + totalTime: 0, + incomingBytes: 0, + outgoingBytes: 0 + }; + } + const isOldData = stat.users.local.inc == null || stat.users.local.dec == null || @@ -180,6 +189,12 @@ export default (params: any) => new Promise(async (res, rej) => { decCount: 0, decSize: 0 } + }, + network: { + requests: 0, + totalTime: 0, + incomingBytes: 0, + outgoingBytes: 0 } }); } else { @@ -236,6 +251,12 @@ export default (params: any) => new Promise(async (res, rej) => { decCount: 0, decSize: 0 } + }, + network: { + requests: 0, + totalTime: 0, + incomingBytes: 0, + outgoingBytes: 0 } }); } diff --git a/src/server/index.ts b/src/server/index.ts index f1fcf58c8d..dc60b0d9ec 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -11,11 +11,13 @@ import * as Router from 'koa-router'; import * as mount from 'koa-mount'; import * as compress from 'koa-compress'; import * as logger from 'koa-logger'; +const requestStats = require('request-stats'); //const slow = require('koa-slow'); import activityPub from './activitypub'; import webFinger from './webfinger'; import config from '../config'; +import { updateNetworkStats } from '../services/update-chart'; // Init app const app = new Koa(); @@ -81,4 +83,27 @@ export default () => new Promise(resolve => { // Listen server.listen(config.port, resolve); + + //#region Network stats + let queue: any[] = []; + + requestStats(server, (stats: any) => { + if (stats.ok) { + queue.push(stats); + } + }); + + // Bulk write + setInterval(() => { + if (queue.length == 0) return; + + const requests = queue.length; + const time = queue.reduce((a, b) => a + b.time, 0); + const incomingBytes = queue.reduce((a, b) => a + b.req.bytes, 0); + const outgoingBytes = queue.reduce((a, b) => a + b.res.bytes, 0); + queue = []; + + updateNetworkStats(requests, time, incomingBytes, outgoingBytes); + }, 5000); + //#endregion }); diff --git a/src/services/update-chart.ts b/src/services/update-chart.ts index 1f8da6be9f..78834ba601 100644 --- a/src/services/update-chart.ts +++ b/src/services/update-chart.ts @@ -96,6 +96,12 @@ async function getCurrentStats(span: 'day' | 'hour'): Promise { decCount: 0, decSize: 0 } + }, + network: { + requests: 0, + totalTime: 0, + incomingBytes: 0, + outgoingBytes: 0 } }; @@ -161,6 +167,12 @@ async function getCurrentStats(span: 'day' | 'hour'): Promise { decCount: 0, decSize: 0 } + }, + network: { + requests: 0, + totalTime: 0, + incomingBytes: 0, + outgoingBytes: 0 } }; @@ -243,3 +255,13 @@ export async function updateDriveStats(file: IDriveFile, isAdditional: boolean) await update(inc); } + +export async function updateNetworkStats(requests: number, time: number, incomingBytes: number, outgoingBytes: number) { + const inc = {} as any; + inc['network.requests'] = requests; + inc['network.totalTime'] = time; + inc['network.incomingBytes'] = incomingBytes; + inc['network.outgoingBytes'] = outgoingBytes; + + await update(inc); +} -- cgit v1.2.3-freya From fc31e44fd2d70ae8f4114009a26f00fc5db01fca Mon Sep 17 00:00:00 2001 From: syuilo Date: Sat, 15 Sep 2018 05:42:14 +0900 Subject: Fix bug --- src/server/api/endpoints/aggregation/hashtags.ts | 2 +- src/server/api/endpoints/hashtags/trend.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src/server/api') diff --git a/src/server/api/endpoints/aggregation/hashtags.ts b/src/server/api/endpoints/aggregation/hashtags.ts index c5aacd89cd..ffeafb2538 100644 --- a/src/server/api/endpoints/aggregation/hashtags.ts +++ b/src/server/api/endpoints/aggregation/hashtags.ts @@ -3,7 +3,7 @@ import Meta from '../../../../models/meta'; export default () => new Promise(async (res, rej) => { const meta = await Meta.findOne({}); - const hidedTags = (meta.hidedTags || []).map(t => t.toLowerCase()); + const hidedTags = meta ? (meta.hidedTags || []).map(t => t.toLowerCase()) : []; const span = 1000 * 60 * 60 * 24 * 7; // 1週間 diff --git a/src/server/api/endpoints/hashtags/trend.ts b/src/server/api/endpoints/hashtags/trend.ts index bfa475619c..0ec6a4ffec 100644 --- a/src/server/api/endpoints/hashtags/trend.ts +++ b/src/server/api/endpoints/hashtags/trend.ts @@ -19,7 +19,7 @@ const max = 5; */ export default () => new Promise(async (res, rej) => { const meta = await Meta.findOne({}); - const hidedTags = (meta.hidedTags || []).map(t => t.toLowerCase()); + const hidedTags = meta ? (meta.hidedTags || []).map(t => t.toLowerCase()) : []; //#region 1. 直近Aの内に投稿されたハッシュタグ(とユーザーのペア)を集計 const data = await Note.aggregate([{ -- cgit v1.2.3-freya From 32afe77a269f414965373e3c53044c4a94cfeded Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 16 Sep 2018 22:48:57 +0900 Subject: 自分宛ての投稿をタイムラインで見れるように MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/ja-JP.yml | 3 ++ .../app/desktop/views/components/timeline.core.vue | 6 ++- .../app/desktop/views/components/timeline.vue | 2 + .../app/mobile/views/pages/home.timeline.vue | 6 ++- src/client/app/mobile/views/pages/home.vue | 3 ++ src/models/note.ts | 2 + src/server/api/endpoints/notes/mentions.ts | 59 ++++++++++++---------- src/services/note/create.ts | 4 ++ 8 files changed, 54 insertions(+), 31 deletions(-) (limited to 'src/server/api') diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 3ced4dafe1..2a8cfebb57 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -167,6 +167,7 @@ common: local: "ローカル" hybrid: "ソーシャル" global: "グローバル" + mentions: "あなた宛て" notifications: "通知" list: "リスト" swap-left: "左に移動" @@ -913,6 +914,7 @@ desktop/views/components/timeline.vue: local: "ローカル" hybrid: "ソーシャル" global: "グローバル" + mentions: "あなた宛て" list: "リスト" desktop/views/components/ui.header.vue: @@ -1314,6 +1316,7 @@ mobile/views/pages/home.vue: local: "ローカル" hybrid: "ソーシャル" global: "グローバル" + mentions: "あなた宛て" mobile/views/pages/tag.vue: no-posts-found: "ハッシュタグ「{}」が付けられた投稿は見つかりませんでした。" diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue index 25fd5d36ac..b6b5cca817 100644 --- a/src/client/app/desktop/views/components/timeline.core.vue +++ b/src/client/app/desktop/views/components/timeline.core.vue @@ -48,6 +48,7 @@ export default Vue.extend({ case 'local': return (this as any).os.streams.localTimelineStream; case 'hybrid': return (this as any).os.streams.hybridTimelineStream; case 'global': return (this as any).os.streams.globalTimelineStream; + case 'mentions': return (this as any).os.stream; } }, @@ -57,6 +58,7 @@ export default Vue.extend({ case 'local': return 'notes/local-timeline'; case 'hybrid': return 'notes/hybrid-timeline'; case 'global': return 'notes/global-timeline'; + case 'mentions': return 'notes/mentions'; } }, @@ -69,7 +71,7 @@ export default Vue.extend({ this.connection = this.stream.getConnection(); this.connectionId = this.stream.use(); - this.connection.on('note', this.onNote); + this.connection.on(this.src == 'mentions' ? 'mention' : 'note', this.onNote); if (this.src == 'home') { this.connection.on('follow', this.onChangeFollowing); this.connection.on('unfollow', this.onChangeFollowing); @@ -81,7 +83,7 @@ export default Vue.extend({ }, beforeDestroy() { - this.connection.off('note', this.onNote); + this.connection.off(this.src == 'mentions' ? 'mention' : 'note', this.onNote); if (this.src == 'home') { this.connection.off('follow', this.onChangeFollowing); this.connection.off('unfollow', this.onChangeFollowing); diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue index 8d72016f22..3e51d12883 100644 --- a/src/client/app/desktop/views/components/timeline.vue +++ b/src/client/app/desktop/views/components/timeline.vue @@ -5,6 +5,7 @@ %fa:R comments% %i18n:@local% %fa:share-alt% %i18n:@hybrid% %fa:globe% %i18n:@global% + %fa:at% %i18n:@mentions% %fa:list% {{ list.title }}
    @@ -12,6 +13,7 @@ +
    diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue index 416b006cd8..d4fcea1f93 100644 --- a/src/client/app/mobile/views/pages/home.timeline.vue +++ b/src/client/app/mobile/views/pages/home.timeline.vue @@ -47,6 +47,7 @@ export default Vue.extend({ case 'local': return (this as any).os.streams.localTimelineStream; case 'hybrid': return (this as any).os.streams.hybridTimelineStream; case 'global': return (this as any).os.streams.globalTimelineStream; + case 'mentions': return (this as any).os.stream; } }, @@ -56,6 +57,7 @@ export default Vue.extend({ case 'local': return 'notes/local-timeline'; case 'hybrid': return 'notes/hybrid-timeline'; case 'global': return 'notes/global-timeline'; + case 'mentions': return 'notes/mentions'; } }, @@ -68,7 +70,7 @@ export default Vue.extend({ this.connection = this.stream.getConnection(); this.connectionId = this.stream.use(); - this.connection.on('note', this.onNote); + this.connection.on(this.src == 'mentions' ? 'mention' : 'note', this.onNote); if (this.src == 'home') { this.connection.on('follow', this.onChangeFollowing); this.connection.on('unfollow', this.onChangeFollowing); @@ -78,7 +80,7 @@ export default Vue.extend({ }, beforeDestroy() { - this.connection.off('note', this.onNote); + this.connection.off(this.src == 'mentions' ? 'mention' : 'note', this.onNote); if (this.src == 'home') { this.connection.off('follow', this.onChangeFollowing); this.connection.off('unfollow', this.onChangeFollowing); diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue index 333ca1a7a1..3150bb02b4 100644 --- a/src/client/app/mobile/views/pages/home.vue +++ b/src/client/app/mobile/views/pages/home.vue @@ -6,6 +6,7 @@ %fa:R comments%%i18n:@local% %fa:share-alt%%i18n:@hybrid% %fa:globe%%i18n:@global% + %fa:at%%i18n:@mentions% %fa:list%{{ list.title }} @@ -27,6 +28,7 @@ %fa:R comments% %i18n:@local% %fa:share-alt% %i18n:@hybrid% %fa:globe% %i18n:@global% + %fa:at% %i18n:@mentions% @@ -39,6 +41,7 @@ +
    diff --git a/src/models/note.ts b/src/models/note.ts index 6530d0b324..62b1b3ecb1 100644 --- a/src/models/note.ts +++ b/src/models/note.ts @@ -17,6 +17,8 @@ import Following from './following'; const Note = db.get('notes'); Note.createIndex('uri', { sparse: true, unique: true }); Note.createIndex('userId'); +Note.createIndex('mentions'); +Note.createIndex('visibleUserIds'); Note.createIndex('tagsLower'); Note.createIndex('_files.contentType'); Note.createIndex({ diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts index a7fb14d8a9..3b2e262e4f 100644 --- a/src/server/api/endpoints/notes/mentions.ts +++ b/src/server/api/endpoints/notes/mentions.ts @@ -3,6 +3,7 @@ import Note from '../../../../models/note'; import { getFriendIds } from '../../common/get-friends'; import { pack } from '../../../../models/note'; import { ILocalUser } from '../../../../models/user'; +import getParams from '../../get-params'; export const meta = { desc: { @@ -10,42 +11,48 @@ export const meta = { 'en-US': 'Get mentions of myself.' }, - requireCredential: true -}; + requireCredential: true, -export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => { - // Get 'following' parameter - const [following = false, followingError] = - $.bool.optional.get(params.following); - if (followingError) return rej('invalid following param'); + params: { + following: $.bool.optional.note({ + default: false + }), + + limit: $.num.optional.range(1, 100).note({ + default: 10 + }), - // Get 'limit' parameter - const [limit = 10, limitErr] = $.num.optional.range(1, 100).get(params.limit); - if (limitErr) return rej('invalid limit param'); + sinceId: $.type(ID).optional.note({ + }), - // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $.type(ID).optional.get(params.sinceId); - if (sinceIdErr) return rej('invalid sinceId param'); + untilId: $.type(ID).optional.note({ + }), + } +}; - // Get 'untilId' parameter - const [untilId, untilIdErr] = $.type(ID).optional.get(params.untilId); - if (untilIdErr) return rej('invalid untilId param'); +export default (params: any, user: ILocalUser) => new Promise(async (res, rej) => { + const [ps, psErr] = getParams(meta, params); + if (psErr) throw psErr; // Check if both of sinceId and untilId is specified - if (sinceId && untilId) { + if (ps.sinceId && ps.untilId) { return rej('cannot set sinceId and untilId'); } // Construct query const query = { - mentions: user._id + $or: [{ + mentions: user._id + }, { + visibleUserIds: user._id + }] } as any; const sort = { _id: -1 }; - if (following) { + if (ps.following) { const followingIds = await getFriendIds(user._id); query.userId = { @@ -53,26 +60,24 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = }; } - if (sinceId) { + if (ps.sinceId) { sort._id = 1; query._id = { - $gt: sinceId + $gt: ps.sinceId }; - } else if (untilId) { + } else if (ps.untilId) { query._id = { - $lt: untilId + $lt: ps.untilId }; } // Issue query const mentions = await Note .find(query, { - limit: limit, + limit: ps.limit, sort: sort }); // Serialize - res(await Promise.all(mentions.map(async mention => - await pack(mention, user) - ))); + res(await Promise.all(mentions.map(mention => pack(mention, user)))); }); diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 771e9cade8..aa65cfe0cf 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -138,6 +138,10 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< const mentionedUsers = await extractMentionedUsers(tokens); + if (data.reply && !user._id.equals(data.reply.userId) && !mentionedUsers.some(u => u._id.equals(data.reply.userId))) { + mentionedUsers.push(await User.findOne({ _id: data.reply.userId })); + } + const note = await insertNote(user, data, tags, mentionedUsers); res(note); -- cgit v1.2.3-freya From 109738ccb9ef8c203685e6f4bc31986ac2a17046 Mon Sep 17 00:00:00 2001 From: syuilo Date: Mon, 17 Sep 2018 09:00:20 +0900 Subject: ハッシュタグタイムラインを実装 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/ja-JP.yml | 5 + src/client/app/common/scripts/streaming/hashtag.ts | 13 +++ .../desktop/views/components/settings-window.vue | 8 +- .../app/desktop/views/components/settings.tags.vue | 65 +++++++++++ .../app/desktop/views/components/settings.vue | 18 +++- .../app/desktop/views/components/timeline.core.vue | 76 +++++++++---- .../app/desktop/views/components/timeline.vue | 119 +++++++++++++++++---- .../desktop/views/pages/deck/deck.column-core.vue | 1 + .../desktop/views/pages/deck/deck.hashtag-tl.vue | 117 ++++++++++++++++++++ .../desktop/views/pages/deck/deck.tl-column.vue | 7 +- src/client/app/desktop/views/pages/deck/deck.vue | 14 +++ .../app/mobile/views/pages/home.timeline.vue | 76 +++++++++---- src/client/app/mobile/views/pages/home.vue | 17 ++- src/client/app/store.ts | 1 + src/server/api/endpoints/notes/search_by_tag.ts | 45 ++++---- src/server/api/stream/hashtag.ts | 48 +++++++++ src/server/api/streaming.ts | 6 ++ src/services/note/create.ts | 6 +- src/stream.ts | 5 + 19 files changed, 555 insertions(+), 92 deletions(-) create mode 100644 src/client/app/common/scripts/streaming/hashtag.ts create mode 100644 src/client/app/desktop/views/components/settings.tags.vue create mode 100644 src/client/app/desktop/views/pages/deck/deck.hashtag-tl.vue create mode 100644 src/server/api/stream/hashtag.ts (limited to 'src/server/api') diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 2a8cfebb57..a3b2bd88e7 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -166,6 +166,7 @@ common: home: "ホーム" local: "ローカル" hybrid: "ソーシャル" + hashtag: "ハッシュタグ" global: "グローバル" mentions: "あなた宛て" notifications: "通知" @@ -916,6 +917,10 @@ desktop/views/components/timeline.vue: global: "グローバル" mentions: "あなた宛て" list: "リスト" + hashtag: "ハッシュタグ" + add-tag-timeline: "ハッシュタグを追加" + add-list: "リストを追加" + list-name: "リスト名" desktop/views/components/ui.header.vue: welcome-back: "おかえりなさい、" diff --git a/src/client/app/common/scripts/streaming/hashtag.ts b/src/client/app/common/scripts/streaming/hashtag.ts new file mode 100644 index 0000000000..276b8f8d3d --- /dev/null +++ b/src/client/app/common/scripts/streaming/hashtag.ts @@ -0,0 +1,13 @@ +import Stream from './stream'; +import MiOS from '../../../mios'; + +export class HashtagStream extends Stream { + constructor(os: MiOS, me, q) { + super(os, 'hashtag', me ? { + i: me.token, + q: JSON.stringify(q) + } : { + q: JSON.stringify(q) + }); + } +} diff --git a/src/client/app/desktop/views/components/settings-window.vue b/src/client/app/desktop/views/components/settings-window.vue index deb865b102..b4cc570282 100644 --- a/src/client/app/desktop/views/components/settings-window.vue +++ b/src/client/app/desktop/views/components/settings-window.vue @@ -1,13 +1,19 @@ + + diff --git a/src/client/app/desktop/views/components/settings.vue b/src/client/app/desktop/views/components/settings.vue index 3911ec5931..312a7ed56e 100644 --- a/src/client/app/desktop/views/components/settings.vue +++ b/src/client/app/desktop/views/components/settings.vue @@ -5,6 +5,7 @@

    %fa:desktop .fw%Web

    %fa:R bell .fw%%i18n:@notification%

    %fa:cloud .fw%%i18n:@drive%

    +

    %fa:hashtag .fw%%i18n:@tags%

    %fa:ban .fw%%i18n:@mute%

    %fa:puzzle-piece .fw%%i18n:@apps%

    %fa:B twitter .fw%Twitter

    @@ -138,6 +139,11 @@ +
    +

    %i18n:@tags%

    + +
    +

    %i18n:@mute%

    @@ -222,6 +228,7 @@ import XApi from './settings.api.vue'; import XApps from './settings.apps.vue'; import XSignins from './settings.signins.vue'; import XDrive from './settings.drive.vue'; +import XTags from './settings.tags.vue'; import { url, langs, version } from '../../../config'; import checkForUpdate from '../../../common/scripts/check-for-update'; @@ -234,11 +241,18 @@ export default Vue.extend({ XApi, XApps, XSignins, - XDrive + XDrive, + XTags + }, + props: { + initialPage: { + type: String, + required: false + } }, data() { return { - page: 'profile', + page: this.initialPage || 'profile', meta: null, version, langs, diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue index b6b5cca817..d2176dee87 100644 --- a/src/client/app/desktop/views/components/timeline.core.vue +++ b/src/client/app/desktop/views/components/timeline.core.vue @@ -15,6 +15,7 @@ diff --git a/src/client/app/desktop/views/pages/deck/deck.tl-column.vue b/src/client/app/desktop/views/pages/deck/deck.tl-column.vue index 231b505f5d..550b1be628 100644 --- a/src/client/app/desktop/views/pages/deck/deck.tl-column.vue +++ b/src/client/app/desktop/views/pages/deck/deck.tl-column.vue @@ -6,6 +6,7 @@ + {{ name }} @@ -14,6 +15,7 @@ + @@ -23,12 +25,14 @@ import Vue from 'vue'; import XColumn from './deck.column.vue'; import XTl from './deck.tl.vue'; import XListTl from './deck.list-tl.vue'; +import XHashtagTl from './deck.hashtag-tl.vue'; export default Vue.extend({ components: { XColumn, XTl, - XListTl + XListTl, + XHashtagTl }, props: { @@ -65,6 +69,7 @@ export default Vue.extend({ case 'hybrid': return '%i18n:common.deck.hybrid%'; case 'global': return '%i18n:common.deck.global%'; case 'list': return this.column.list.title; + case 'hashtag': return this.$store.state.settings.tagTimelines.find(x => x.id == this.column.tagTlId).title; } } }, diff --git a/src/client/app/desktop/views/pages/deck/deck.vue b/src/client/app/desktop/views/pages/deck/deck.vue index 4a4535959e..aafe9a45d3 100644 --- a/src/client/app/desktop/views/pages/deck/deck.vue +++ b/src/client/app/desktop/views/pages/deck/deck.vue @@ -161,6 +161,20 @@ export default Vue.extend({ w.close(); }); } + }, { + icon: '%fa:hashtag%', + text: '%i18n:common.deck.hashtag%', + action: () => { + (this as any).apis.input({ + title: '%i18n:@enter-hashtag-tl-title%' + }).then(title => { + this.$store.dispatch('settings/addDeckColumn', { + id: uuid(), + type: 'hashtag', + tagTlId: this.$store.state.settings.tagTimelines.find(x => x.title == title).id + }); + }); + } }, { icon: '%fa:bell R%', text: '%i18n:common.deck.notifications%', diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue index d4fcea1f93..fecb2384ba 100644 --- a/src/client/app/mobile/views/pages/home.timeline.vue +++ b/src/client/app/mobile/views/pages/home.timeline.vue @@ -13,6 +13,7 @@ diff --git a/src/client/app/desktop/views/pages/deck/deck.direct.vue b/src/client/app/desktop/views/pages/deck/deck.direct.vue new file mode 100644 index 0000000000..ec9e6b9c3d --- /dev/null +++ b/src/client/app/desktop/views/pages/deck/deck.direct.vue @@ -0,0 +1,97 @@ + + + diff --git a/src/client/app/desktop/views/pages/deck/deck.vue b/src/client/app/desktop/views/pages/deck/deck.vue index aafe9a45d3..e5aeba251a 100644 --- a/src/client/app/desktop/views/pages/deck/deck.vue +++ b/src/client/app/desktop/views/pages/deck/deck.vue @@ -147,6 +147,15 @@ export default Vue.extend({ type: 'mentions' }); } + }, { + icon: '%fa:envelope R%', + text: '%i18n:common.deck.direct%', + action: () => { + this.$store.dispatch('settings/addDeckColumn', { + id: uuid(), + type: 'direct' + }); + } }, { icon: '%fa:list%', text: '%i18n:common.deck.list%', diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue index fecb2384ba..225abcff6b 100644 --- a/src/client/app/mobile/views/pages/home.timeline.vue +++ b/src/client/app/mobile/views/pages/home.timeline.vue @@ -37,7 +37,14 @@ export default Vue.extend({ connection: null, connectionId: null, unreadCount: 0, - date: null + date: null, + baseQuery: { + includeMyRenotes: this.$store.state.settings.showMyRenotes, + includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, + includeLocalRenotes: this.$store.state.settings.showLocalRenotes + }, + query: {}, + endpoint: null }; }, @@ -46,80 +53,109 @@ export default Vue.extend({ return this.$store.state.i.followingCount == 0; }, - endpoint(): string { - switch (this.src) { - case 'home': return 'notes/timeline'; - case 'local': return 'notes/local-timeline'; - case 'hybrid': return 'notes/hybrid-timeline'; - case 'global': return 'notes/global-timeline'; - case 'mentions': return 'notes/mentions'; - case 'tag': return 'notes/search_by_tag'; - } - }, - canFetchMore(): boolean { return !this.moreFetching && !this.fetching && this.existMore; } }, mounted() { + const prepend = note => { + (this.$refs.timeline as any).prepend(note); + }; + if (this.src == 'tag') { + this.endpoint = 'notes/search_by_tag'; + this.query = { + query: this.tagTl.query + }; this.connection = new HashtagStream((this as any).os, this.$store.state.i, this.tagTl.query); - this.connection.on('note', this.onNote); + this.connection.on('note', prepend); + this.$once('beforeDestroy', () => { + this.connection.off('note', prepend); + this.connection.close(); + }); } else if (this.src == 'home') { + this.endpoint = 'notes/timeline'; + const onChangeFollowing = () => { + this.fetch(); + }; this.streamManager = (this as any).os.stream; this.connection = this.streamManager.getConnection(); this.connectionId = this.streamManager.use(); - this.connection.on('note', this.onNote); - this.connection.on('follow', this.onChangeFollowing); - this.connection.on('unfollow', this.onChangeFollowing); + this.connection.on('note', prepend); + this.connection.on('follow', onChangeFollowing); + this.connection.on('unfollow', onChangeFollowing); + this.$once('beforeDestroy', () => { + this.connection.off('note', prepend); + this.connection.off('follow', onChangeFollowing); + this.connection.off('unfollow', onChangeFollowing); + this.streamManager.dispose(this.connectionId); + }); } else if (this.src == 'local') { + this.endpoint = 'notes/local-timeline'; this.streamManager = (this as any).os.streams.localTimelineStream; this.connection = this.streamManager.getConnection(); this.connectionId = this.streamManager.use(); - this.connection.on('note', this.onNote); + this.connection.on('note', prepend); + this.$once('beforeDestroy', () => { + this.connection.off('note', prepend); + this.streamManager.dispose(this.connectionId); + }); } else if (this.src == 'hybrid') { + this.endpoint = 'notes/hybrid-timeline'; this.streamManager = (this as any).os.streams.hybridTimelineStream; this.connection = this.streamManager.getConnection(); this.connectionId = this.streamManager.use(); - this.connection.on('note', this.onNote); + this.connection.on('note', prepend); + this.$once('beforeDestroy', () => { + this.connection.off('note', prepend); + this.streamManager.dispose(this.connectionId); + }); } else if (this.src == 'global') { + this.endpoint = 'notes/global-timeline'; this.streamManager = (this as any).os.streams.globalTimelineStream; this.connection = this.streamManager.getConnection(); this.connectionId = this.streamManager.use(); - this.connection.on('note', this.onNote); + this.connection.on('note', prepend); + this.$once('beforeDestroy', () => { + this.connection.off('note', prepend); + this.streamManager.dispose(this.connectionId); + }); } else if (this.src == 'mentions') { + this.endpoint = 'notes/mentions'; + this.streamManager = (this as any).os.stream; + this.connection = this.streamManager.getConnection(); + this.connectionId = this.streamManager.use(); + this.connection.on('mention', prepend); + this.$once('beforeDestroy', () => { + this.connection.off('mention', prepend); + this.streamManager.dispose(this.connectionId); + }); + } else if (this.src == 'messages') { + this.endpoint = 'notes/mentions'; + this.query = { + visibility: 'specified' + }; + const onNote = note => { + if (note.visibility == 'specified') { + prepend(note); + } + }; this.streamManager = (this as any).os.stream; this.connection = this.streamManager.getConnection(); this.connectionId = this.streamManager.use(); - this.connection.on('mention', this.onNote); + this.connection.on('mention', onNote); + this.$once('beforeDestroy', () => { + this.connection.off('mention', onNote); + this.streamManager.dispose(this.connectionId); + }); } this.fetch(); }, beforeDestroy() { - if (this.src == 'tag') { - this.connection.off('note', this.onNote); - this.connection.close(); - } else if (this.src == 'home') { - this.connection.off('note', this.onNote); - this.connection.off('follow', this.onChangeFollowing); - this.connection.off('unfollow', this.onChangeFollowing); - this.streamManager.dispose(this.connectionId); - } else if (this.src == 'local') { - this.connection.off('note', this.onNote); - this.streamManager.dispose(this.connectionId); - } else if (this.src == 'hybrid') { - this.connection.off('note', this.onNote); - this.streamManager.dispose(this.connectionId); - } else if (this.src == 'global') { - this.connection.off('note', this.onNote); - this.streamManager.dispose(this.connectionId); - } else if (this.src == 'mentions') { - this.connection.off('mention', this.onNote); - this.streamManager.dispose(this.connectionId); - } + this.$emit('beforeDestroy'); }, methods: { @@ -127,14 +163,10 @@ export default Vue.extend({ this.fetching = true; (this.$refs.timeline as any).init(() => new Promise((res, rej) => { - (this as any).api(this.endpoint, { + (this as any).api(this.endpoint, Object.assign({ limit: fetchLimit + 1, - untilDate: this.date ? this.date.getTime() : undefined, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes, - query: this.tagTl ? this.tagTl.query : undefined - }).then(notes => { + untilDate: this.date ? this.date.getTime() : undefined + }, this.baseQuery, this.query)).then(notes => { if (notes.length == fetchLimit + 1) { notes.pop(); this.existMore = true; @@ -151,14 +183,10 @@ export default Vue.extend({ this.moreFetching = true; - const promise = (this as any).api(this.endpoint, { + const promise = (this as any).api(this.endpoint, Object.assign({ limit: fetchLimit + 1, - untilId: (this.$refs.timeline as any).tail().id, - includeMyRenotes: this.$store.state.settings.showMyRenotes, - includeRenotedMyNotes: this.$store.state.settings.showRenotedMyNotes, - includeLocalRenotes: this.$store.state.settings.showLocalRenotes, - query: this.tagTl ? this.tagTl.query : undefined - }); + untilId: (this.$refs.timeline as any).tail().id + }, this.baseQuery, this.query)); promise.then(notes => { if (notes.length == fetchLimit + 1) { @@ -173,15 +201,6 @@ export default Vue.extend({ return promise; }, - onNote(note) { - // Prepend a note - (this.$refs.timeline as any).prepend(note); - }, - - onChangeFollowing() { - this.fetch(); - }, - focus() { (this.$refs.timeline as any).focus(); }, diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue index 3ec2f16b75..e61916fe18 100644 --- a/src/client/app/mobile/views/pages/home.vue +++ b/src/client/app/mobile/views/pages/home.vue @@ -7,6 +7,7 @@ %fa:share-alt%%i18n:@hybrid% %fa:globe%%i18n:@global% %fa:at%%i18n:@mentions% + %fa:envelope R%%i18n:@messages% %fa:list%{{ list.title }} %fa:hashtag%{{ tagTl.title }} @@ -23,16 +24,21 @@
    @@ -150,6 +157,26 @@ export default Vue.extend({ root(isDark) > .nav + > .pointer + position fixed + z-index 10002 + top 56px + left 0 + right 0 + + $size = 16px + + &:after + content "" + display block + position absolute + top -($size * 2) + left s('calc(50% - %s)', $size) + border-top solid $size transparent + border-left solid $size transparent + border-right solid $size transparent + border-bottom solid $size isDark ? #272f3a : #fff + > .bg position fixed z-index 10000 @@ -166,28 +193,22 @@ root(isDark) left 0 right 0 width 300px + max-height calc(100% - 70px) margin 0 auto + overflow auto + -webkit-overflow-scrolling touch background isDark ? #272f3a : #fff border-radius 8px box-shadow 0 0 16px rgba(#000, 0.1) - $balloon-size = 16px - - &:after - content "" - display block - position absolute - top -($balloon-size * 2) + 1.5px - left s('calc(50% - %s)', $balloon-size) - border-top solid $balloon-size transparent - border-left solid $balloon-size transparent - border-right solid $balloon-size transparent - border-bottom solid $balloon-size isDark ? #272f3a : #fff - > div padding 8px 0 - > * + > .hr + margin 8px 0 + border-top solid 1px isDark ? rgba(#000, 0.3) : rgba(#000, 0.1) + + > *:not(.hr) display block padding 8px 16px color isDark ? #cdd0d8 : #666 diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts index 3b2e262e4f..8675a9f562 100644 --- a/src/server/api/endpoints/notes/mentions.ts +++ b/src/server/api/endpoints/notes/mentions.ts @@ -27,6 +27,9 @@ export const meta = { untilId: $.type(ID).optional.note({ }), + + visibility: $.str.optional.note({ + }), } }; @@ -52,6 +55,10 @@ export default (params: any, user: ILocalUser) => new Promise(async (res, rej) = _id: -1 }; + if (ps.visibility) { + query.visibility = ps.visibility; + } + if (ps.following) { const followingIds = await getFriendIds(user._id); diff --git a/src/services/note/create.ts b/src/services/note/create.ts index 7daf83b294..7c1e71dcb3 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -142,6 +142,14 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< mentionedUsers.push(await User.findOne({ _id: data.reply.userId })); } + if (data.visibility == 'specified') { + data.visibleUsers.forEach(u => { + if (!mentionedUsers.some(x => x._id.equals(u._id))) { + mentionedUsers.push(u); + } + }); + } + const note = await insertNote(user, data, tags, mentionedUsers); res(note); @@ -188,7 +196,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise< const nm = new NotificationManager(user, note); const nmRelatedPromises = []; - createMentionedEvents(mentionedUsers, noteObj, nm); + createMentionedEvents(mentionedUsers, note, nm); const noteActivity = await renderActivity(data, note); @@ -318,7 +326,7 @@ async function publish(user: IUser, note: INote, noteObj: any, reply: INote, ren if (['public', 'home', 'followers'].includes(note.visibility)) { // フォロワーに配信 - publishToFollowers(note, noteObj, user, noteActivity); + publishToFollowers(note, user, noteActivity); } // リストに配信 @@ -456,7 +464,7 @@ async function publishToUserLists(note: INote, noteObj: any) { }); } -async function publishToFollowers(note: INote, noteObj: any, user: IUser, noteActivity: any) { +async function publishToFollowers(note: INote, user: IUser, noteActivity: any) { const detailPackedNote = await pack(note, null, { detail: true, skipHide: true @@ -505,9 +513,13 @@ function deliverNoteToMentionedRemoteUsers(mentionedUsers: IUser[], user: ILocal }); } -function createMentionedEvents(mentionedUsers: IUser[], noteObj: any, nm: NotificationManager) { +function createMentionedEvents(mentionedUsers: IUser[], note: INote, nm: NotificationManager) { mentionedUsers.filter(u => isLocalUser(u)).forEach(async (u) => { - publishUserStream(u._id, 'mention', noteObj); + const detailPackedNote = await pack(note, u, { + detail: true + }); + + publishUserStream(u._id, 'mention', detailPackedNote); // Create notification nm.push(u._id, 'mention'); -- cgit v1.2.3-freya From 1f2ebce8ed749d7e81e999944fc8a22ff39b87b7 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 18 Sep 2018 06:29:47 +0900 Subject: Resolve #1302 --- src/client/app/desktop/views/pages/user/user.vue | 2 +- src/client/app/mobile/views/pages/user/home.vue | 2 +- src/docs/api/entities/user.yaml | 8 +++--- src/models/user.ts | 32 ++++++++++++++++++++---- src/server/api/endpoints/i/pin.ts | 14 ++++++++++- 5 files changed, 46 insertions(+), 12 deletions(-) (limited to 'src/server/api') diff --git a/src/client/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue index 28ccd78074..89dbd41b84 100644 --- a/src/client/app/desktop/views/pages/user/user.vue +++ b/src/client/app/desktop/views/pages/user/user.vue @@ -6,7 +6,7 @@
    - +
    diff --git a/src/client/app/mobile/views/pages/user/home.vue b/src/client/app/mobile/views/pages/user/home.vue index 8b57276b17..4118afef19 100644 --- a/src/client/app/mobile/views/pages/user/home.vue +++ b/src/client/app/mobile/views/pages/user/home.vue @@ -1,6 +1,6 @@