diff --git a/.eslintrc b/.eslintrc index 7a74d6ef9bf8b91b54a34000db0e6ba22291ecfa..0943cb4b6480eb6f91b67be4fc2d7092a0bb4be2 100644 --- a/.eslintrc +++ b/.eslintrc @@ -14,6 +14,7 @@ "vue/no-unused-vars": false, "vue/attributes-order": false, "vue/require-prop-types": false, + "vue/require-default-prop": false, "no-console": 0, "no-unused-vars": 0, "no-empty": 0 diff --git a/locales/en.yml b/locales/en.yml index 0a053932273c020489392b45b6cd018ad69ac9a8..4ef12432be4449fae0c988e67a6be5379926eec6 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -370,6 +370,7 @@ desktop/views/components/ui.header.account.vue: profile: "Your profile" drive: "Drive" favorites: "Favorites" + lists: "Lists" customize: "Customize" settings: "Settings" signout: "Sign out" diff --git a/locales/fr.yml b/locales/fr.yml index e640c4883ce90d248fad857b67249580bcf40572..80b1ed13da9e28fd51a00fe38e50fe3dd1123d4e 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -370,6 +370,7 @@ desktop/views/components/ui.header.account.vue: profile: "Votre profil" drive: "Drive" favorites: "Favorites" + lists: "リスト" customize: "Modifications" settings: "Réglages" signout: "Déconnexion" diff --git a/locales/ja.yml b/locales/ja.yml index 3d023281cd4945f2f67866ee09dafc524a65a3f8..e13348c4070acc7518d721dad872ddd638827f25 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -370,6 +370,7 @@ desktop/views/components/ui.header.account.vue: profile: "プãƒãƒ•ã‚£ãƒ¼ãƒ«" drive: "ドライブ" favorites: "ãŠæ°—ã«å…¥ã‚Š" + lists: "リスト" customize: "カスタマイズ" settings: "è¨å®š" signout: "サインアウト" diff --git a/package.json b/package.json index 0a3026e17e802efb897f60e4970b6a1c59fc92d5..d37bbc040d6c1e94ba2c682a8551213c58fac911 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "autwh": "0.1.0", "bcryptjs": "2.4.3", "bootstrap-vue": "2.0.0-rc.6", - "cafy": "3.2.1", + "cafy": "6.0.0", "chai": "4.1.2", "chai-http": "4.0.0", "chalk": "2.4.0", @@ -144,6 +144,7 @@ "koa-multer": "1.0.2", "koa-router": "7.4.0", "koa-send": "4.1.3", + "koa-slow": "^2.1.0", "kue": "0.11.6", "license-checker": "18.0.0", "loader-utils": "1.1.0", diff --git a/src/cafy-id.ts b/src/cafy-id.ts new file mode 100644 index 0000000000000000000000000000000000000000..3faf5cd996c59979533931d1427ac7bdaa880937 --- /dev/null +++ b/src/cafy-id.ts @@ -0,0 +1,29 @@ +import * as mongo from 'mongodb'; +import { Query } from 'cafy'; + +export const isAnId = x => mongo.ObjectID.isValid(x); +export const isNotAnId = x => !isAnId(x); + +/** + * ID + */ +export default class ID extends Query<mongo.ObjectID> { + constructor(...args) { + super(...args); + + this.transform = v => { + if (isAnId(v) && !mongo.ObjectID.prototype.isPrototypeOf(v)) { + return new mongo.ObjectID(v); + } else { + return v; + } + }; + + this.pushValidator(v => { + if (!mongo.ObjectID.prototype.isPrototypeOf(v) && isNotAnId(v)) { + return new Error('must-be-an-id'); + } + return true; + }); + } +} diff --git a/src/client/app/common/mios.ts b/src/client/app/common/mios.ts index 463f76388847d0fc44621985ea77f4006865afc7..4e471cf96f71a09f8559b4e2c4e4025876297585 100644 --- a/src/client/app/common/mios.ts +++ b/src/client/app/common/mios.ts @@ -88,6 +88,7 @@ export default class MiOS extends EventEmitter { propsData: props }).$mount(); document.body.appendChild(w.$el); + return w; } /** diff --git a/src/client/app/common/scripts/streaming/user-list.ts b/src/client/app/common/scripts/streaming/user-list.ts new file mode 100644 index 0000000000000000000000000000000000000000..30a52b98dd8b8c87b98c8cd251d246e1df5d5516 --- /dev/null +++ b/src/client/app/common/scripts/streaming/user-list.ts @@ -0,0 +1,17 @@ +import Stream from './stream'; +import MiOS from '../../mios'; + +export class UserListStream extends Stream { + constructor(os: MiOS, me, listId) { + super(os, 'user-list', { + i: me.token, + listId + }); + + (this as any).on('_connected_', () => { + this.send({ + i: me.token + }); + }); + } +} diff --git a/src/client/app/desktop/api/update-banner.ts b/src/client/app/desktop/api/update-banner.ts index bc3f783e357b21fad930774fec69ea7dcdd9da84..feb1c33103c42f91072efc966ec527e5b909605f 100644 --- a/src/client/app/desktop/api/update-banner.ts +++ b/src/client/app/desktop/api/update-banner.ts @@ -95,7 +95,7 @@ export default (os: OS) => { multiple: false, title: '%fa:image%ãƒãƒŠãƒ¼ã«ã™ã‚‹ç”»åƒã‚’é¸æŠž' }); - + return selectedFile .then(cropImage) .then(setBanner) diff --git a/src/client/app/desktop/script.ts b/src/client/app/desktop/script.ts index 3b0ed48cd0d668a561cbd45fcd5a746b53d4e9d5..2658a86b9505f090eb56012874deb24781cb7c10 100644 --- a/src/client/app/desktop/script.ts +++ b/src/client/app/desktop/script.ts @@ -28,6 +28,7 @@ import MkUser from './views/pages/user/user.vue'; import MkFavorites from './views/pages/favorites.vue'; import MkSelectDrive from './views/pages/selectdrive.vue'; import MkDrive from './views/pages/drive.vue'; +import MkUserList from './views/pages/user-list.vue'; import MkHomeCustomize from './views/pages/home-customize.vue'; import MkMessagingRoom from './views/pages/messaging-room.vue'; import MkNote from './views/pages/note.vue'; @@ -55,6 +56,7 @@ init(async (launch) => { { path: '/i/messaging/:user', component: MkMessagingRoom }, { path: '/i/drive', component: MkDrive }, { path: '/i/drive/folder/:folder', component: MkDrive }, + { path: '/i/lists/:list', component: MkUserList }, { path: '/selectdrive', component: MkSelectDrive }, { path: '/search', component: MkSearch }, { path: '/othello', component: MkOthello }, diff --git a/src/client/app/desktop/views/components/follow-button.vue b/src/client/app/desktop/views/components/follow-button.vue index 30e8cab76f2d59898f1b85443906de4e67a66f21..60c6129f61a1ea568e4db666c529cde4d7c2cbc9 100644 --- a/src/client/app/desktop/views/components/follow-button.vue +++ b/src/client/app/desktop/views/components/follow-button.vue @@ -19,6 +19,7 @@ <script lang="ts"> import Vue from 'vue'; + export default Vue.extend({ props: { user: { @@ -30,6 +31,7 @@ export default Vue.extend({ default: 'compact' } }, + data() { return { wait: false, @@ -37,6 +39,7 @@ export default Vue.extend({ connectionId: null }; }, + mounted() { this.connection = (this as any).os.stream.getConnection(); this.connectionId = (this as any).os.stream.use(); @@ -44,13 +47,14 @@ export default Vue.extend({ this.connection.on('follow', this.onFollow); this.connection.on('unfollow', this.onUnfollow); }, + beforeDestroy() { this.connection.off('follow', this.onFollow); this.connection.off('unfollow', this.onUnfollow); (this as any).os.stream.dispose(this.connectionId); }, - methods: { + methods: { onFollow(user) { if (user.id == this.user.id) { this.user.isFollowing = user.isFollowing; diff --git a/src/client/app/desktop/views/components/index.ts b/src/client/app/desktop/views/components/index.ts index 4f61f43692f76e95de3901de799cbfa810814d09..f58d0706df25f9b7d272faaa7fc178971b4b50c5 100644 --- a/src/client/app/desktop/views/components/index.ts +++ b/src/client/app/desktop/views/components/index.ts @@ -28,6 +28,7 @@ import friendsMaker from './friends-maker.vue'; import followers from './followers.vue'; import following from './following.vue'; import usersList from './users-list.vue'; +import userListTimeline from './user-list-timeline.vue'; import widgetContainer from './widget-container.vue'; Vue.component('mk-ui', ui); @@ -58,4 +59,5 @@ Vue.component('mk-friends-maker', friendsMaker); Vue.component('mk-followers', followers); Vue.component('mk-following', following); Vue.component('mk-users-list', usersList); +Vue.component('mk-user-list-timeline', userListTimeline); Vue.component('mk-widget-container', widgetContainer); diff --git a/src/client/app/desktop/views/components/mentions.vue b/src/client/app/desktop/views/components/mentions.vue index fc3a7af75d776880dc7c1a4cabb5bcbc042de140..53d08a0eca9b4c129bec9ce914d3fc8d1f53d51d 100644 --- a/src/client/app/desktop/views/components/mentions.vue +++ b/src/client/app/desktop/views/components/mentions.vue @@ -1,8 +1,8 @@ <template> <div class="mk-mentions"> <header> - <span :data-is-active="mode == 'all'" @click="mode = 'all'">ã™ã¹ã¦</span> - <span :data-is-active="mode == 'following'" @click="mode = 'following'">フォãƒãƒ¼ä¸</span> + <span :data-active="mode == 'all'" @click="mode = 'all'">ã™ã¹ã¦</span> + <span :data-active="mode == 'following'" @click="mode = 'following'">フォãƒãƒ¼ä¸</span> </header> <div class="fetching" v-if="fetching"> <mk-ellipsis-icon/> @@ -98,7 +98,7 @@ export default Vue.extend({ font-size 18px color #555 - &:not([data-is-active]) + &:not([data-active]) color $theme-color cursor pointer diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue index 1a33a4240b4c64ab9bd35a207b5482663c7f6458..fa7a782b7b42c820be476e9bcb72d5a88c934a38 100644 --- a/src/client/app/desktop/views/components/notes.vue +++ b/src/client/app/desktop/views/components/notes.vue @@ -1,5 +1,14 @@ <template> <div class="mk-notes"> + <div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div> + + <slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot> + + <div v-if="!fetching && requestInitPromise != null"> + <p>èªã¿è¾¼ã¿ã«å¤±æ•—ã—ã¾ã—ãŸã€‚</p> + <button @click="resolveInitPromise">リトライ</button> + </div> + <transition-group name="mk-notes" class="transition"> <template v-for="(note, i) in _notes"> <x-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> @@ -9,26 +18,48 @@ </p> </template> </transition-group> - <footer> - <slot name="footer"></slot> + + <footer v-if="more"> + <button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> + <template v-if="!moreFetching">%i18n:@load-more%</template> + <template v-if="moreFetching">%fa:spinner .pulse .fw%</template> + </button> </footer> </div> </template> <script lang="ts"> import Vue from 'vue'; +import { url } from '../../../config'; +import getNoteSummary from '../../../../../renderers/get-note-summary'; + import XNote from './notes.note.vue'; +const displayLimit = 30; + export default Vue.extend({ components: { XNote }, + props: { - notes: { - type: Array, - default: () => [] + more: { + type: Function, + required: false } }, + + data() { + return { + requestInitPromise: null as () => Promise<any[]>, + notes: [], + queue: [], + unreadCount: 0, + fetching: true, + moreFetching: false + }; + }, + computed: { _notes(): any[] { return (this.notes as any).map(note => { @@ -40,18 +71,146 @@ export default Vue.extend({ }); } }, + + mounted() { + document.addEventListener('visibilitychange', this.onVisibilitychange, false); + window.addEventListener('scroll', this.onScroll); + }, + + beforeDestroy() { + document.removeEventListener('visibilitychange', this.onVisibilitychange); + window.removeEventListener('scroll', this.onScroll); + }, + methods: { + isScrollTop() { + return window.scrollY <= 8; + }, + focus() { (this.$el as any).children[0].focus(); }, + onNoteUpdated(i, note) { Vue.set((this as any).notes, i, note); + }, + + init(promiseGenerator: () => Promise<any[]>) { + this.requestInitPromise = promiseGenerator; + this.resolveInitPromise(); + }, + + resolveInitPromise() { + this.queue = []; + this.notes = []; + this.fetching = true; + + const promise = this.requestInitPromise(); + + promise.then(notes => { + this.notes = notes; + this.requestInitPromise = null; + this.fetching = false; + }, e => { + this.fetching = false; + }); + }, + + prepend(note, silent = false) { + //#region å¼¾ã + const isMyNote = note.userId == (this as any).os.i.id; + const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null; + + if ((this as any).os.i.clientSettings.showMyRenotes === false) { + if (isMyNote && isPureRenote) { + return; + } + } + + if ((this as any).os.i.clientSettings.showRenotedMyNotes === false) { + if (isPureRenote && (note.renote.userId == (this as any).os.i.id)) { + return; + } + } + //#endregion + + // 投稿ãŒè‡ªåˆ†ã®ã‚‚ã®ã§ã¯ãªã„ã‹ã¤ã€ã‚¿ãƒ–ãŒéžè¡¨ç¤ºã¾ãŸã¯ã‚¹ã‚¯ãƒãƒ¼ãƒ«ä½ç½®ãŒæœ€ä¸Šéƒ¨ã§ã¯ãªã„ãªã‚‰ã‚¿ã‚¤ãƒˆãƒ«ã§é€šçŸ¥ + if ((document.hidden || !this.isScrollTop()) && note.userId !== (this as any).os.i.id) { + this.unreadCount++; + document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`; + } + + if (this.isScrollTop()) { + // Prepend the note + this.notes.unshift(note); + + // サウンドをå†ç”Ÿã™ã‚‹ + if ((this as any).os.isEnableSounds && !silent) { + const sound = new Audio(`${url}/assets/post.mp3`); + sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5; + sound.play(); + } + + // オーãƒãƒ¼ãƒ•ãƒãƒ¼ã—ãŸã‚‰å¤ã„投稿ã¯æ¨ã¦ã‚‹ + if (this.notes.length >= displayLimit) { + this.notes = this.notes.slice(0, displayLimit); + } + } else { + this.queue.unshift(note); + } + }, + + append(note) { + this.notes.push(note); + }, + + tail() { + return this.notes[this.notes.length - 1]; + }, + + releaseQueue() { + this.queue.forEach(n => this.prepend(n, true)); + this.queue = []; + }, + + async loadMore() { + if (this.more == null) return; + if (this.moreFetching) return; + + this.moreFetching = true; + await this.more(); + this.moreFetching = false; + }, + + clearNotification() { + this.unreadCount = 0; + document.title = 'Misskey'; + }, + + onVisibilitychange() { + if (!document.hidden) { + this.clearNotification(); + } + }, + + onScroll() { + if (this.isScrollTop()) { + this.releaseQueue(); + this.clearNotification(); + } + + if ((this as any).os.i.clientSettings.fetchOnScroll !== false) { + const current = window.scrollY + window.innerHeight; + if (current > document.body.offsetHeight - 8) this.loadMore(); + } } } }); </script> <style lang="stylus" scoped> +@import '~const.styl' + root(isDark) .transition .mk-notes-enter @@ -78,24 +237,31 @@ root(isDark) [data-fa] margin-right 8px + > .newer-indicator + position -webkit-sticky + position sticky + z-index 100 + height 3px + background $theme-color + > footer - > * + > button display block margin 0 padding 16px width 100% text-align center color #ccc - border-top solid 1px #eaeaea - border-bottom-left-radius 4px - border-bottom-right-radius 4px + background isDark ? #282C37 : #fff + border-top solid 1px isDark ? #1c2023 : #eaeaea + border-bottom-left-radius 6px + border-bottom-right-radius 6px - > button &:hover - background #f5f5f5 + background isDark ? #2e3440 : #f5f5f5 &:active - background #eee + background isDark ? #21242b : #eee .mk-notes[data-darkmode] root(true) diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue index 719425c3c717bc39db677a12d178e5e7586d8b14..a137a57070fbabe6b04901e0eb2dcfab36d91eb8 100644 --- a/src/client/app/desktop/views/components/timeline.core.vue +++ b/src/client/app/desktop/views/components/timeline.core.vue @@ -1,28 +1,23 @@ <template> -<div class="mk-home-timeline"> - <div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div> +<div class="mk-timeline-core"> <mk-friends-maker v-if="src == 'home' && alone"/> <div class="fetching" v-if="fetching"> <mk-ellipsis-icon/> </div> - <p class="empty" v-if="notes.length == 0 && !fetching"> - %fa:R comments%%i18n:@empty% - </p> - <mk-notes :notes="notes" ref="timeline"> - <button slot="footer" @click="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> - <template v-if="!moreFetching">%i18n:@load-more%</template> - <template v-if="moreFetching">%fa:spinner .pulse .fw%</template> - </button> + + <mk-notes ref="timeline" :more="canFetchMore ? more : null"> + <p :class="$style.empty" slot="empty"> + %fa:R comments%%i18n:@empty% + </p> </mk-notes> </div> </template> <script lang="ts"> import Vue from 'vue'; -import { url } from '../../../config'; +import getNoteSummary from '../../../../../renderers/get-note-summary'; const fetchLimit = 10; -const displayLimit = 30; export default Vue.extend({ props: { @@ -37,10 +32,9 @@ export default Vue.extend({ fetching: true, moreFetching: false, existMore: false, - notes: [], - queue: [], connection: null, connectionId: null, + unreadCount: 0, date: null }; }, @@ -67,7 +61,7 @@ export default Vue.extend({ }, canFetchMore(): boolean { - return !this.moreFetching && !this.fetching && this.notes.length > 0 && this.existMore; + return !this.moreFetching && !this.fetching && this.existMore; } }, @@ -82,7 +76,7 @@ export default Vue.extend({ } document.addEventListener('keydown', this.onKeydown); - window.addEventListener('scroll', this.onScroll); + document.addEventListener('visibilitychange', this.onVisibilitychange, false); this.fetch(); }, @@ -96,33 +90,29 @@ export default Vue.extend({ this.stream.dispose(this.connectionId); document.removeEventListener('keydown', this.onKeydown); - window.removeEventListener('scroll', this.onScroll); + document.removeEventListener('visibilitychange', this.onVisibilitychange); }, methods: { - isScrollTop() { - return window.scrollY <= 8; - }, - - fetch(cb?) { - this.queue = []; + fetch() { this.fetching = true; - (this as any).api(this.endpoint, { - limit: fetchLimit + 1, - untilDate: this.date ? this.date.getTime() : undefined, - includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes, - includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes - }).then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - this.existMore = true; - } - this.notes = notes; - this.fetching = false; - this.$emit('loaded'); - if (cb) cb(); - }); + (this.$refs.timeline as any).init(() => new Promise((res, rej) => { + (this as any).api(this.endpoint, { + limit: fetchLimit + 1, + untilDate: this.date ? this.date.getTime() : undefined, + includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes, + includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + this.existMore = true; + } + res(notes); + this.fetching = false; + this.$emit('loaded'); + }, rej); + })); }, more() { @@ -132,7 +122,7 @@ export default Vue.extend({ (this as any).api(this.endpoint, { limit: fetchLimit + 1, - untilId: this.notes[this.notes.length - 1].id, + untilId: (this.$refs.timeline as any).tail().id, includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes, includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes }).then(notes => { @@ -141,56 +131,19 @@ export default Vue.extend({ } else { this.existMore = false; } - this.notes = this.notes.concat(notes); + notes.forEach(n => (this.$refs.timeline as any).append(n)); this.moreFetching = false; }); }, - prependNote(note, silent = false) { - // サウンドをå†ç”Ÿã™ã‚‹ - if ((this as any).os.isEnableSounds && !silent) { - const sound = new Audio(`${url}/assets/post.mp3`); - sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5; - sound.play(); - } - - // Prepent a note - this.notes.unshift(note); - - // オーãƒãƒ¼ãƒ•ãƒãƒ¼ã—ãŸã‚‰å¤ã„投稿ã¯æ¨ã¦ã‚‹ - if (this.notes.length >= displayLimit) { - this.notes = this.notes.slice(0, displayLimit); - } - }, - - releaseQueue() { - this.queue.forEach(n => this.prependNote(n, true)); - this.queue = []; - }, - onNote(note) { - //#region å¼¾ã - const isMyNote = note.userId == (this as any).os.i.id; - const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null; - - if ((this as any).os.i.clientSettings.showMyRenotes === false) { - if (isMyNote && isPureRenote) { - return; - } + if (document.hidden && note.userId !== (this as any).os.i.id) { + this.unreadCount++; + document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`; } - if ((this as any).os.i.clientSettings.showRenotedMyNotes === false) { - if (isPureRenote && (note.renote.userId == (this as any).os.i.id)) { - return; - } - } - //#endregion - - if (this.isScrollTop()) { - this.prependNote(note); - } else { - this.queue.unshift(note); - } + // Prepend a note + (this.$refs.timeline as any).prepend(note); }, onChangeFollowing() { @@ -206,14 +159,10 @@ export default Vue.extend({ this.fetch(); }, - onScroll() { - if ((this as any).os.i.clientSettings.fetchOnScroll !== false) { - const current = window.scrollY + window.innerHeight; - if (current > document.body.offsetHeight - 8) this.more(); - } - - if (this.isScrollTop()) { - this.releaseQueue(); + onVisibilitychange() { + if (!document.hidden) { + this.unreadCount = 0; + document.title = 'Misskey'; } }, @@ -223,7 +172,7 @@ export default Vue.extend({ this.focus(); } } - }, + } } }); </script> @@ -231,32 +180,28 @@ export default Vue.extend({ <style lang="stylus" scoped> @import '~const.styl' -.mk-home-timeline - > .newer-indicator - position -webkit-sticky - position sticky - z-index 100 - height 3px - background $theme-color - +.mk-timeline-core > .mk-friends-maker border-bottom solid 1px #eee > .fetching padding 64px 0 - > .empty +</style> + +<style lang="stylus" module> +.empty + display block + margin 0 auto + padding 32px + max-width 400px + text-align center + color #999 + + > [data-fa] display block - margin 0 auto - padding 32px - max-width 400px - text-align center - color #999 - - > [data-fa] - display block - margin-bottom 16px - font-size 3em - color #ccc + margin-bottom 16px + font-size 3em + color #ccc </style> diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue index 8035510a1435e92baa7b58cc95eb678d19a63ed9..f5f13cbd56dbf10aa3e8271c48385c12909c94bd 100644 --- a/src/client/app/desktop/views/components/timeline.vue +++ b/src/client/app/desktop/views/components/timeline.vue @@ -1,19 +1,23 @@ <template> <div class="mk-timeline"> <header> - <span :data-is-active="src == 'home'" @click="src = 'home'">%fa:home% ホーム</span> - <span :data-is-active="src == 'local'" @click="src = 'local'">%fa:R comments% ãƒãƒ¼ã‚«ãƒ«</span> - <span :data-is-active="src == 'global'" @click="src = 'global'">%fa:globe% ã‚°ãƒãƒ¼ãƒãƒ«</span> + <span :data-active="src == 'home'" @click="src = 'home'">%fa:home% ホーム</span> + <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% ãƒãƒ¼ã‚«ãƒ«</span> + <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% ã‚°ãƒãƒ¼ãƒãƒ«</span> + <span :data-active="src == 'list'" @click="src = 'list'" v-if="list">%fa:list% {{ list.title }}</span> + <button @click="chooseList" title="リスト">%fa:list%</button> </header> <x-core v-if="src == 'home'" ref="tl" key="home" src="home"/> <x-core v-if="src == 'local'" ref="tl" key="local" src="local"/> <x-core v-if="src == 'global'" ref="tl" key="global" src="global"/> + <mk-user-list-timeline v-if="src == 'list'" ref="tl" key="list" :list="list"/> </div> </template> <script lang="ts"> import Vue from 'vue'; import XCore from './timeline.core.vue'; +import MkUserListsWindow from './user-lists-window.vue'; export default Vue.extend({ components: { @@ -22,7 +26,8 @@ export default Vue.extend({ data() { return { - src: 'home' + src: 'home', + list: null }; }, @@ -35,6 +40,15 @@ export default Vue.extend({ methods: { warp(date) { (this.$refs.tl as any).warp(date); + }, + + chooseList() { + const w = (this as any).os.new(MkUserListsWindow); + w.$once('choosen', list => { + this.list = list; + this.src = 'list'; + w.close(); + }); } } }); @@ -55,6 +69,23 @@ root(isDark) border-radius 6px 6px 0 0 box-shadow 0 1px rgba(0, 0, 0, 0.08) + > button + position absolute + z-index 2 + top 0 + right 0 + padding 0 + width 42px + font-size 0.9em + line-height 42px + color isDark ? #9baec8 : #ccc + + &:hover + color isDark ? #b2c1d5 : #aaa + + &:active + color isDark ? #b2c1d5 : #999 + > span display inline-block padding 0 10px @@ -62,7 +93,7 @@ root(isDark) font-size 12px user-select none - &[data-is-active] + &[data-active] color $theme-color cursor default font-weight bold @@ -77,7 +108,7 @@ root(isDark) height 2px background $theme-color - &:not([data-is-active]) + &:not([data-active]) color isDark ? #9aa2a7 : #6f7477 cursor pointer diff --git a/src/client/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue index 2d4d23933c7d1af6953161945965479311dd84c6..5148c5b967664446305a04b43ff9e28d9073ec2e 100644 --- a/src/client/app/desktop/views/components/ui.header.account.vue +++ b/src/client/app/desktop/views/components/ui.header.account.vue @@ -16,6 +16,9 @@ <li> <router-link to="/i/favorites">%fa:star%<span>%i18n:@favorites%</span>%fa:angle-right%</router-link> </li> + <li @click="list"> + <p>%fa:list%<span>%i18n:@lists%</span>%fa:angle-right%</p> + </li> </ul> <ul> <li> @@ -42,6 +45,7 @@ <script lang="ts"> import Vue from 'vue'; +import MkUserListsWindow from './user-lists-window.vue'; import MkSettingsWindow from './settings-window.vue'; import MkDriveWindow from './drive-window.vue'; import contains from '../../../common/scripts/contains'; @@ -80,6 +84,13 @@ export default Vue.extend({ this.close(); (this as any).os.new(MkDriveWindow); }, + list() { + this.close(); + const w = (this as any).os.new(MkUserListsWindow); + w.$once('choosen', list => { + this.$router.push(`i/lists/${ list.id }`); + }); + }, settings() { this.close(); (this as any).os.new(MkSettingsWindow); diff --git a/src/client/app/desktop/views/components/user-list-timeline.vue b/src/client/app/desktop/views/components/user-list-timeline.vue new file mode 100644 index 0000000000000000000000000000000000000000..ee983a969ca99ee64abaa0136a777f2d537f3855 --- /dev/null +++ b/src/client/app/desktop/views/components/user-list-timeline.vue @@ -0,0 +1,93 @@ +<template> +<div> + <mk-notes ref="timeline" :more="existMore ? more : null"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { UserListStream } from '../../../common/scripts/streaming/user-list'; + +const fetchLimit = 10; + +export default Vue.extend({ + props: ['list'], + data() { + return { + fetching: true, + moreFetching: false, + existMore: false, + connection: null + }; + }, + watch: { + $route: 'init' + }, + mounted() { + this.init(); + }, + beforeDestroy() { + this.connection.close(); + }, + methods: { + init() { + if (this.connection) this.connection.close(); + this.connection = new UserListStream((this as any).os, (this as any).os.i, this.list.id); + this.connection.on('note', this.onNote); + this.connection.on('userAdded', this.onUserAdded); + this.connection.on('userRemoved', this.onUserRemoved); + + this.fetch(); + }, + fetch() { + this.fetching = true; + + (this.$refs.timeline as any).init(() => new Promise((res, rej) => { + (this as any).api('notes/user-list-timeline', { + listId: this.list.id, + limit: fetchLimit + 1, + includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes, + includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + this.existMore = true; + } + res(notes); + this.fetching = false; + this.$emit('loaded'); + }, rej); + })); + }, + more() { + this.moreFetching = true; + + (this as any).api('notes/list-timeline', { + listId: this.list.id, + limit: fetchLimit + 1, + untilId: (this.$refs.timeline as any).tail().id, + includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes, + includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + } else { + this.existMore = false; + } + notes.forEach(n => (this.$refs.timeline as any).append(n)); + this.moreFetching = false; + }); + }, + onNote(note) { + // Prepend a note + (this.$refs.timeline as any).prepend(note); + }, + onUserAdded() { + this.fetch(); + }, + onUserRemoved() { + this.fetch(); + } + } +}); +</script> diff --git a/src/client/app/desktop/views/components/user-lists-window.vue b/src/client/app/desktop/views/components/user-lists-window.vue new file mode 100644 index 0000000000000000000000000000000000000000..d082610132a46abd159a7a60ad6fef38f36b293e --- /dev/null +++ b/src/client/app/desktop/views/components/user-lists-window.vue @@ -0,0 +1,69 @@ +<template> +<mk-window ref="window" is-modal width="450px" height="500px" @closed="$destroy"> + <span slot="header">%fa:list% リスト</span> + + <div data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82" :data-darkmode="_darkmode_"> + <button class="ui" @click="add">リストを作æˆ</button> + <a v-for="list in lists" :key="list.id" @click="choice(list)">{{ list.title }}</a> + </div> +</mk-window> +</template> + +<script lang="ts"> +import Vue from 'vue'; +export default Vue.extend({ + data() { + return { + fetching: true, + lists: [] + }; + }, + mounted() { + (this as any).api('users/lists/list').then(lists => { + this.fetching = false; + this.lists = lists; + }); + }, + methods: { + add() { + (this as any).apis.input({ + title: 'リストå', + }).then(async title => { + const list = await (this as any).api('users/lists/create', { + title + }); + + this.$emit('choosen', list); + }); + }, + choice(list) { + this.$emit('choosen', list); + }, + close() { + (this as any).$refs.window.close(); + } + } +}); +</script> + +<style lang="stylus" scoped> + +root(isDark) + padding 16px + + > button + margin-bottom 16px + + > a + display block + padding 16px + border solid 1px isDark ? #1c2023 : #eee + border-radius 4px + +[data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82"][data-darkmode] + root(true) + +[data-id="6e4caea3-d8f9-4ab7-96de-ab67fe8d5c82"]:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/desktop/views/components/users-list.vue b/src/client/app/desktop/views/components/users-list.vue index a08e76f573ed3cef0d55a6882f2d02c2e34d4de3..e8f4c94d426a7ef7eb840aef8b3ed47dc8d0a3b8 100644 --- a/src/client/app/desktop/views/components/users-list.vue +++ b/src/client/app/desktop/views/components/users-list.vue @@ -2,8 +2,8 @@ <div class="mk-users-list"> <nav> <div> - <span :data-is-active="mode == 'all'" @click="mode = 'all'">ã™ã¹ã¦<span>{{ count }}</span></span> - <span v-if="os.isSignedIn && youKnowCount" :data-is-active="mode == 'iknow'" @click="mode = 'iknow'">知りåˆã„<span>{{ youKnowCount }}</span></span> + <span :data-active="mode == 'all'" @click="mode = 'all'">ã™ã¹ã¦<span>{{ count }}</span></span> + <span v-if="os.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">知りåˆã„<span>{{ youKnowCount }}</span></span> </div> </nav> <div class="users" v-if="!fetching && users.length != 0"> @@ -98,7 +98,7 @@ export default Vue.extend({ * pointer-events none - &[data-is-active] + &[data-active] font-weight bold color $theme-color border-color $theme-color diff --git a/src/client/app/desktop/views/components/widget-container.vue b/src/client/app/desktop/views/components/widget-container.vue index c3fac1399d72d20b43ad5fc68168e0e51c1c9601..926d7702b9d6e7b41caecb3178966a8d92cf2d1f 100644 --- a/src/client/app/desktop/views/components/widget-container.vue +++ b/src/client/app/desktop/views/components/widget-container.vue @@ -58,7 +58,7 @@ root(isDark) box-shadow 0 1px rgba(0, 0, 0, 0.07) > [data-fa] - margin-right 4px + margin-right 6px &:empty display none diff --git a/src/client/app/desktop/views/pages/user-list.users.vue b/src/client/app/desktop/views/pages/user-list.users.vue new file mode 100644 index 0000000000000000000000000000000000000000..63070ed609b7d7575bd5e81e1900864990750f3f --- /dev/null +++ b/src/client/app/desktop/views/pages/user-list.users.vue @@ -0,0 +1,131 @@ +<template> +<div> + <mk-widget-container> + <template slot="header">%fa:users% ユーザー</template> + <button slot="func" title="ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’è¿½åŠ " @click="add">%fa:plus%</button> + + <div data-id="d0b63759-a822-4556-a5ce-373ab966e08a"> + <p class="fetching" v-if="fetching">%fa:spinner .pulse .fw% %i18n:common.loading%<mk-ellipsis/></p> + <template v-else-if="users.length != 0"> + <div class="user" v-for="_user in users"> + <router-link class="avatar-anchor" :to="_user | userPage"> + <img class="avatar" :src="`${_user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/> + </router-link> + <div class="body"> + <router-link class="name" :to="_user | userPage" v-user-preview="_user.id">{{ _user | userName }}</router-link> + <p class="username">@{{ _user | acct }}</p> + </div> + </div> + </template> + <p class="empty" v-else>%i18n:@no-one%</p> + </div> + </mk-widget-container> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +export default Vue.extend({ + props: { + list: { + type: Object, + required: true + } + }, + data() { + return { + fetching: true, + users: [] + }; + }, + mounted() { + (this as any).api('users/show', { + userIds: this.list.userIds + }).then(users => { + this.users = users; + this.fetching = false; + }); + }, + methods: { + add() { + (this as any).apis.input({ + title: 'ユーザーå', + }).then(async username => { + const user = await (this as any).api('users/show', { + username + }); + + (this as any).api('users/lists/push', { + listId: this.list.id, + userId: user.id + }); + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +root(isDark) + > .user + padding 16px + border-bottom solid 1px isDark ? #1c2023 : #eee + + &:last-child + border-bottom none + + &:after + content "" + display block + clear both + + > .avatar-anchor + display block + float left + margin 0 12px 0 0 + + > .avatar + display block + width 42px + height 42px + margin 0 + border-radius 8px + vertical-align bottom + + > .body + float left + width calc(100% - 54px) + + > .name + margin 0 + font-size 16px + line-height 24px + color isDark ? #fff : #555 + + > .username + display block + margin 0 + font-size 15px + line-height 16px + color isDark ? #606984 : #ccc + + > .empty + margin 0 + padding 16px + text-align center + color #aaa + + > .fetching + margin 0 + padding 16px + text-align center + color #aaa + +[data-id="d0b63759-a822-4556-a5ce-373ab966e08a"][data-darkmode] + root(true) + +[data-id="d0b63759-a822-4556-a5ce-373ab966e08a"]:not([data-darkmode]) + root(false) + +</style> diff --git a/src/client/app/desktop/views/pages/user-list.vue b/src/client/app/desktop/views/pages/user-list.vue new file mode 100644 index 0000000000000000000000000000000000000000..2241b84e5e2716e4a91fc940e43ab98322ed4e38 --- /dev/null +++ b/src/client/app/desktop/views/pages/user-list.vue @@ -0,0 +1,71 @@ +<template> +<mk-ui> + <div v-if="!fetching" data-id="02010e15-cc48-4245-8636-16078a9b623c"> + <div> + <div><h1>{{ list.title }}</h1></div> + <x-users :list="list"/> + </div> + <main> + <mk-user-list-timeline :list="list"/> + </main> + </div> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import XUsers from './user-list.users.vue'; + +export default Vue.extend({ + components: { + XUsers + }, + data() { + return { + fetching: true, + list: null + }; + }, + watch: { + $route: 'fetch' + }, + mounted() { + this.fetch(); + }, + methods: { + fetch() { + this.fetching = true; + + (this as any).api('users/lists/show', { + listId: this.$route.params.list + }).then(list => { + this.list = list; + this.fetching = false; + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +[data-id="02010e15-cc48-4245-8636-16078a9b623c"] + display flex + justify-content center + margin 0 auto + max-width 1200px + + > main + > div > div + > *:not(:last-child) + margin-bottom 16px + + > main + padding 16px + width calc(100% - 275px * 2) + + > div + width 275px + margin 0 + padding 16px 0 16px 16px + +</style> diff --git a/src/client/app/desktop/views/pages/user/user.profile.vue b/src/client/app/desktop/views/pages/user/user.profile.vue index 774f300a38b0da58d553f4ee2a67e4804d1e9b8d..64acbd86b36f713fa156346d98d7adf804b912b4 100644 --- a/src/client/app/desktop/views/pages/user/user.profile.vue +++ b/src/client/app/desktop/views/pages/user/user.profile.vue @@ -3,15 +3,18 @@ <div class="friend-form" v-if="os.isSignedIn && os.i.id != user.id"> <mk-follow-button :user="user" size="big"/> <p class="followed" v-if="user.isFollowed">%i18n:@follows-you%</p> - <p class="stalk"> - <span v-if="user.isStalking">%i18n:@stalking% <a @click="unstalk">%i18n:@unstalk%</a></span> - <span v-if="!user.isStalking"><a @click="stalk">%i18n:@stalk%</a></span> - </p> - <p class="mute"> - <span v-if="user.isMuted">%i18n:@muted% <a @click="unmute">%i18n:@unmute%</a></span> - <span v-if="!user.isMuted"><a @click="mute">%i18n:@mute%</a></span> + <p class="stalk" v-if="user.isFollowing"> + <span v-if="user.isStalking">%i18n:@stalking% <a @click="unstalk">%fa:meh% %i18n:@unstalk%</a></span> + <span v-if="!user.isStalking"><a @click="stalk">%fa:user-secret% %i18n:@stalk%</a></span> </p> </div> + <div class="action-form"> + <button class="mute ui" @click="user.isMuted ? unmute() : mute()"> + <span v-if="user.isMuted">%fa:eye% %i18n:@unmute%</span> + <span v-if="!user.isMuted">%fa:eye-slash% %i18n:@mute%</span> + </button> + <button class="mute ui" @click="list">%fa:list% リストã«è¿½åŠ </button> + </div> <div class="description" v-if="user.description">{{ user.description }}</div> <div class="birthday" v-if="user.host === null && user.profile.birthday"> <p>%fa:birthday-cake%{{ user.profile.birthday.replace('-', 'å¹´').replace('-', '月') + 'æ—¥' }} ({{ age }}æ³)</p> @@ -32,6 +35,7 @@ import Vue from 'vue'; import * as age from 's-age'; import MkFollowingWindow from '../../components/following-window.vue'; import MkFollowersWindow from '../../components/followers-window.vue'; +import MkUserListsWindow from '../../components/user-lists-window.vue'; export default Vue.extend({ props: ['user'], @@ -91,6 +95,21 @@ export default Vue.extend({ }, () => { alert('error'); }); + }, + + list() { + const w = (this as any).os.new(MkUserListsWindow); + w.$once('choosen', async list => { + w.close(); + await (this as any).api('users/lists/push', { + listId: list.id, + userId: this.user.id + }); + (this as any).apis.dialog({ + title: 'Done!', + text: `${this.user.name}ã‚’${list.title}ã«è¿½åŠ ã—ã¾ã—ãŸã€‚` + }); + }); } } }); @@ -107,11 +126,9 @@ export default Vue.extend({ > .friend-form padding 16px + text-align center border-top solid 1px #eee - > .mk-big-follow-button - width 100% - > .followed margin 12px 0 0 0 padding 0 @@ -122,6 +139,20 @@ export default Vue.extend({ background #eefaff border-radius 4px + > .stalk + margin 12px 0 0 0 + + > .action-form + padding 16px + text-align center + border-top solid 1px #eee + + > * + width 100% + + &:not(:last-child) + margin-bottom 12px + > .description padding 16px color #555 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 55d6072a9d07f22ca318080c23e61ca492041807..9c9840c1903be9fbb96c1bdb470838a2f13cb04e 100644 --- a/src/client/app/desktop/views/pages/user/user.timeline.vue +++ b/src/client/app/desktop/views/pages/user/user.timeline.vue @@ -1,42 +1,36 @@ <template> <div class="timeline"> <header> - <span :data-is-active="mode == 'default'" @click="mode = 'default'">投稿</span> - <span :data-is-active="mode == 'with-replies'" @click="mode = 'with-replies'">投稿ã¨è¿”ä¿¡</span> - <span :data-is-active="mode == 'with-media'" @click="mode = 'with-media'">メディア</span> + <span :data-active="mode == 'default'" @click="mode = 'default'">投稿</span> + <span :data-active="mode == 'with-replies'" @click="mode = 'with-replies'">投稿ã¨è¿”ä¿¡</span> + <span :data-active="mode == 'with-media'" @click="mode = 'with-media'">メディア</span> </header> <div class="loading" v-if="fetching"> <mk-ellipsis-icon/> </div> - <p class="empty" v-if="empty">%fa:R comments%ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ã¾ã 何も投稿ã—ã¦ã„ãªã„よã†ã§ã™ã€‚</p> - <mk-notes ref="timeline" :notes="notes"> - <div slot="footer"> - <template v-if="!moreFetching">%fa:moon%</template> - <template v-if="moreFetching">%fa:spinner .pulse .fw%</template> - </div> + <mk-notes ref="timeline" :more="existMore ? more : null"> + <p class="empty" slot="empty">%fa:R comments%ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã¯ã¾ã 何も投稿ã—ã¦ã„ãªã„よã†ã§ã™ã€‚</p> </mk-notes> </div> </template> <script lang="ts"> import Vue from 'vue'; + +const fetchLimit = 10; + export default Vue.extend({ props: ['user'], data() { return { fetching: true, moreFetching: false, + existMore: false, mode: 'default', unreadCount: 0, - notes: [], date: null }; }, - computed: { - empty(): boolean { - return this.notes.length == 0; - } - }, watch: { mode() { this.fetch(); @@ -44,13 +38,11 @@ export default Vue.extend({ }, mounted() { document.addEventListener('keydown', this.onDocumentKeydown); - window.addEventListener('scroll', this.onScroll); this.fetch(() => this.$emit('loaded')); }, beforeDestroy() { document.removeEventListener('keydown', this.onDocumentKeydown); - window.removeEventListener('scroll', this.onScroll); }, methods: { onDocumentKeydown(e) { @@ -61,36 +53,43 @@ export default Vue.extend({ } }, fetch(cb?) { - (this as any).api('users/notes', { - userId: this.user.id, - untilDate: this.date ? this.date.getTime() : undefined, - includeReplies: this.mode == 'with-replies', - withMedia: this.mode == 'with-media' - }).then(notes => { - this.notes = notes; - this.fetching = false; - if (cb) cb(); - }); + this.fetching = true; + (this.$refs.timeline as any).init(() => new Promise((res, rej) => { + (this as any).api('users/notes', { + userId: this.user.id, + limit: fetchLimit + 1, + untilDate: this.date ? this.date.getTime() : undefined, + includeReplies: this.mode == 'with-replies', + withMedia: this.mode == 'with-media' + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + this.existMore = true; + } + res(notes); + this.fetching = false; + if (cb) cb(); + }, rej); + })); }, more() { - if (this.moreFetching || this.fetching || this.notes.length == 0) return; this.moreFetching = true; (this as any).api('users/notes', { userId: this.user.id, + limit: fetchLimit + 1, includeReplies: this.mode == 'with-replies', withMedia: this.mode == 'with-media', - untilId: this.notes[this.notes.length - 1].id + untilId: (this.$refs.timeline as any).tail().id }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + } else { + this.existMore = false; + } + notes.forEach(n => (this.$refs.timeline as any).append(n)); this.moreFetching = false; - this.notes = this.notes.concat(notes); }); }, - onScroll() { - const current = window.scrollY + window.innerHeight; - if (current > document.body.offsetHeight - 16/*éŠã³*/) { - this.more(); - } - }, warp(date) { this.date = date; this.fetch(); @@ -115,7 +114,7 @@ export default Vue.extend({ font-size 18px color #555 - &:not([data-is-active]) + &:not([data-active]) color $theme-color cursor pointer diff --git a/src/client/app/mobile/views/components/index.ts b/src/client/app/mobile/views/components/index.ts index 9346700304fe0838241f238438aaa0bb22d475ab..5ed8427b05abd98d845d078f614c75661fbf1020 100644 --- a/src/client/app/mobile/views/components/index.ts +++ b/src/client/app/mobile/views/components/index.ts @@ -1,7 +1,6 @@ import Vue from 'vue'; import ui from './ui.vue'; -import timeline from './timeline.vue'; import note from './note.vue'; import notes from './notes.vue'; import mediaImage from './media-image.vue'; @@ -20,11 +19,11 @@ import notificationPreview from './notification-preview.vue'; import usersList from './users-list.vue'; import userPreview from './user-preview.vue'; import userTimeline from './user-timeline.vue'; +import userListTimeline from './user-list-timeline.vue'; import activity from './activity.vue'; import widgetContainer from './widget-container.vue'; Vue.component('mk-ui', ui); -Vue.component('mk-timeline', timeline); Vue.component('mk-note', note); Vue.component('mk-notes', notes); Vue.component('mk-media-image', mediaImage); @@ -43,5 +42,6 @@ Vue.component('mk-notification-preview', notificationPreview); Vue.component('mk-users-list', usersList); Vue.component('mk-user-preview', userPreview); Vue.component('mk-user-timeline', userTimeline); +Vue.component('mk-user-list-timeline', userListTimeline); Vue.component('mk-activity', activity); Vue.component('mk-widget-container', widgetContainer); diff --git a/src/client/app/mobile/views/components/notes.vue b/src/client/app/mobile/views/components/notes.vue index 999ab566ac0462fa7dda73278611774f2ea58167..703b51d678f06e9e938126612d0e093814744730 100644 --- a/src/client/app/mobile/views/components/notes.vue +++ b/src/client/app/mobile/views/components/notes.vue @@ -1,7 +1,20 @@ <template> <div class="mk-notes"> + <div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div> + <slot name="head"></slot> - <slot></slot> + + <slot name="empty" v-if="notes.length == 0 && !fetching && requestInitPromise == null"></slot> + + <div class="init" v-if="fetching"> + %fa:spinner .pulse%%i18n:common.loading% + </div> + + <div v-if="!fetching && requestInitPromise != null"> + <p>èªã¿è¾¼ã¿ã«å¤±æ•—ã—ã¾ã—ãŸã€‚</p> + <button @click="resolveInitPromise">リトライ</button> + </div> + <transition-group name="mk-notes" class="transition"> <template v-for="(note, i) in _notes"> <mk-note :note="note" :key="note.id" @update:note="onNoteUpdated(i, $event)"/> @@ -11,22 +24,41 @@ </p> </template> </transition-group> - <footer> - <slot name="tail"></slot> + + <footer v-if="more"> + <button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }"> + <template v-if="!moreFetching">%i18n:@load-more%</template> + <template v-if="moreFetching">%fa:spinner .pulse .fw%</template> + </button> </footer> </div> </template> <script lang="ts"> import Vue from 'vue'; +import getNoteSummary from '../../../../../renderers/get-note-summary'; + +const displayLimit = 30; export default Vue.extend({ props: { - notes: { - type: Array, - default: () => [] + more: { + type: Function, + required: false } }, + + data() { + return { + requestInitPromise: null as () => Promise<any[]>, + notes: [], + queue: [], + unreadCount: 0, + fetching: true, + moreFetching: false + }; + }, + computed: { _notes(): any[] { return (this.notes as any).map(note => { @@ -38,9 +70,127 @@ export default Vue.extend({ }); } }, + + mounted() { + document.addEventListener('visibilitychange', this.onVisibilitychange, false); + window.addEventListener('scroll', this.onScroll); + }, + + beforeDestroy() { + document.removeEventListener('visibilitychange', this.onVisibilitychange); + window.removeEventListener('scroll', this.onScroll); + }, + methods: { + isScrollTop() { + return window.scrollY <= 8; + }, + onNoteUpdated(i, note) { Vue.set((this as any).notes, i, note); + }, + + init(promiseGenerator: () => Promise<any[]>) { + this.requestInitPromise = promiseGenerator; + this.resolveInitPromise(); + }, + + resolveInitPromise() { + this.queue = []; + this.notes = []; + this.fetching = true; + + const promise = this.requestInitPromise(); + + promise.then(notes => { + this.notes = notes; + this.requestInitPromise = null; + this.fetching = false; + }, e => { + this.fetching = false; + }); + }, + + prepend(note, silent = false) { + //#region å¼¾ã + const isMyNote = note.userId == (this as any).os.i.id; + const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null; + + if ((this as any).os.i.clientSettings.showMyRenotes === false) { + if (isMyNote && isPureRenote) { + return; + } + } + + if ((this as any).os.i.clientSettings.showRenotedMyNotes === false) { + if (isPureRenote && (note.renote.userId == (this as any).os.i.id)) { + return; + } + } + //#endregion + + // 投稿ãŒè‡ªåˆ†ã®ã‚‚ã®ã§ã¯ãªã„ã‹ã¤ã€ã‚¿ãƒ–ãŒéžè¡¨ç¤ºã¾ãŸã¯ã‚¹ã‚¯ãƒãƒ¼ãƒ«ä½ç½®ãŒæœ€ä¸Šéƒ¨ã§ã¯ãªã„ãªã‚‰ã‚¿ã‚¤ãƒˆãƒ«ã§é€šçŸ¥ + if ((document.hidden || !this.isScrollTop()) && note.userId !== (this as any).os.i.id) { + this.unreadCount++; + document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`; + } + + if (this.isScrollTop()) { + // Prepend the note + this.notes.unshift(note); + + // オーãƒãƒ¼ãƒ•ãƒãƒ¼ã—ãŸã‚‰å¤ã„投稿ã¯æ¨ã¦ã‚‹ + if (this.notes.length >= displayLimit) { + this.notes = this.notes.slice(0, displayLimit); + } + } else { + this.queue.unshift(note); + } + }, + + append(note) { + this.notes.push(note); + }, + + tail() { + return this.notes[this.notes.length - 1]; + }, + + releaseQueue() { + this.queue.forEach(n => this.prepend(n, true)); + this.queue = []; + }, + + async loadMore() { + if (this.more == null) return; + if (this.moreFetching) return; + + this.moreFetching = true; + await this.more(); + this.moreFetching = false; + }, + + clearNotification() { + this.unreadCount = 0; + document.title = 'Misskey'; + }, + + onVisibilitychange() { + if (!document.hidden) { + this.clearNotification(); + } + }, + + onScroll() { + if (this.isScrollTop()) { + this.releaseQueue(); + this.clearNotification(); + } + + if ((this as any).os.i.clientSettings.fetchOnScroll !== false) { + const current = window.scrollY + window.innerHeight; + if (current > document.body.offsetHeight - 8) this.loadMore(); + } } } }); @@ -79,6 +229,13 @@ export default Vue.extend({ [data-fa] margin-right 8px + > .newer-indicator + position -webkit-sticky + position sticky + z-index 100 + height 3px + background $theme-color + > .init padding 64px 0 text-align center diff --git a/src/client/app/mobile/views/components/timeline.vue b/src/client/app/mobile/views/components/timeline.vue deleted file mode 100644 index f56667bed5c1f3771195ab6ee4a66ccefe8ff74c..0000000000000000000000000000000000000000 --- a/src/client/app/mobile/views/components/timeline.vue +++ /dev/null @@ -1,190 +0,0 @@ -<template> -<div class="mk-timeline"> - <div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div> - <mk-friends-maker v-if="alone"/> - <mk-notes :notes="notes"> - <div class="init" v-if="fetching"> - %fa:spinner .pulse%%i18n:common.loading% - </div> - <div class="empty" v-if="!fetching && notes.length == 0"> - %fa:R comments% - %i18n:@empty% - </div> - <button v-if="canFetchMore" @click="more" :disabled="moreFetching" slot="tail"> - <span v-if="!moreFetching">%i18n:@load-more%</span> - <span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span> - </button> - </mk-notes> -</div> -</template> - -<script lang="ts"> -import Vue from 'vue'; - -const fetchLimit = 10; -const displayLimit = 30; - -export default Vue.extend({ - props: { - date: { - type: Date, - required: false, - default: null - } - }, - - data() { - return { - fetching: true, - moreFetching: false, - notes: [], - queue: [], - existMore: false, - connection: null, - connectionId: null - }; - }, - - computed: { - alone(): boolean { - return (this as any).os.i.followingCount == 0; - }, - - canFetchMore(): boolean { - return !this.moreFetching && !this.fetching && this.notes.length > 0 && this.existMore; - } - }, - - mounted() { - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('note', this.onNote); - this.connection.on('follow', this.onChangeFollowing); - this.connection.on('unfollow', this.onChangeFollowing); - - window.addEventListener('scroll', this.onScroll); - - this.fetch(); - }, - - beforeDestroy() { - this.connection.off('note', this.onNote); - this.connection.off('follow', this.onChangeFollowing); - this.connection.off('unfollow', this.onChangeFollowing); - (this as any).os.stream.dispose(this.connectionId); - - window.removeEventListener('scroll', this.onScroll); - }, - - methods: { - isScrollTop() { - return window.scrollY <= 8; - }, - - fetch(cb?) { - this.queue = []; - this.fetching = true; - (this as any).api('notes/timeline', { - limit: fetchLimit + 1, - untilDate: this.date ? (this.date as any).getTime() : undefined, - includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes, - includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes - }).then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - this.existMore = true; - } - this.notes = notes; - this.fetching = false; - this.$emit('loaded'); - if (cb) cb(); - }); - }, - - more() { - this.moreFetching = true; - (this as any).api('notes/timeline', { - limit: fetchLimit + 1, - untilId: this.notes[this.notes.length - 1].id, - includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes, - includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes - }).then(notes => { - if (notes.length == fetchLimit + 1) { - notes.pop(); - this.existMore = true; - } else { - this.existMore = false; - } - this.notes = this.notes.concat(notes); - this.moreFetching = false; - }); - }, - - prependNote(note) { - // Prepent a note - this.notes.unshift(note); - - // オーãƒãƒ¼ãƒ•ãƒãƒ¼ã—ãŸã‚‰å¤ã„投稿ã¯æ¨ã¦ã‚‹ - if (this.notes.length >= displayLimit) { - this.notes = this.notes.slice(0, displayLimit); - } - }, - - releaseQueue() { - this.queue.forEach(n => this.prependNote(n)); - this.queue = []; - }, - - onNote(note) { - //#region å¼¾ã - const isMyNote = note.userId == (this as any).os.i.id; - const isPureRenote = note.renoteId != null && note.text == null && note.mediaIds.length == 0 && note.poll == null; - - if ((this as any).os.i.clientSettings.showMyRenotes === false) { - if (isMyNote && isPureRenote) { - return; - } - } - - if ((this as any).os.i.clientSettings.showRenotedMyNotes === false) { - if (isPureRenote && (note.renote.userId == (this as any).os.i.id)) { - return; - } - } - //#endregion - - if (this.isScrollTop()) { - this.prependNote(note); - } else { - this.queue.unshift(note); - } - }, - - onChangeFollowing() { - this.fetch(); - }, - - onScroll() { - if (this.isScrollTop()) { - this.releaseQueue(); - } - } - } -}); -</script> - -<style lang="stylus" scoped> -@import '~const.styl' - -.mk-timeline - > .newer-indicator - position -webkit-sticky - position sticky - z-index 100 - height 3px - background $theme-color - - > .mk-friends-maker - margin-bottom 8px -</style> diff --git a/src/client/app/mobile/views/components/user-list-timeline.vue b/src/client/app/mobile/views/components/user-list-timeline.vue new file mode 100644 index 0000000000000000000000000000000000000000..ee983a969ca99ee64abaa0136a777f2d537f3855 --- /dev/null +++ b/src/client/app/mobile/views/components/user-list-timeline.vue @@ -0,0 +1,93 @@ +<template> +<div> + <mk-notes ref="timeline" :more="existMore ? more : null"/> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import { UserListStream } from '../../../common/scripts/streaming/user-list'; + +const fetchLimit = 10; + +export default Vue.extend({ + props: ['list'], + data() { + return { + fetching: true, + moreFetching: false, + existMore: false, + connection: null + }; + }, + watch: { + $route: 'init' + }, + mounted() { + this.init(); + }, + beforeDestroy() { + this.connection.close(); + }, + methods: { + init() { + if (this.connection) this.connection.close(); + this.connection = new UserListStream((this as any).os, (this as any).os.i, this.list.id); + this.connection.on('note', this.onNote); + this.connection.on('userAdded', this.onUserAdded); + this.connection.on('userRemoved', this.onUserRemoved); + + this.fetch(); + }, + fetch() { + this.fetching = true; + + (this.$refs.timeline as any).init(() => new Promise((res, rej) => { + (this as any).api('notes/user-list-timeline', { + listId: this.list.id, + limit: fetchLimit + 1, + includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes, + includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + this.existMore = true; + } + res(notes); + this.fetching = false; + this.$emit('loaded'); + }, rej); + })); + }, + more() { + this.moreFetching = true; + + (this as any).api('notes/list-timeline', { + listId: this.list.id, + limit: fetchLimit + 1, + untilId: (this.$refs.timeline as any).tail().id, + includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes, + includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + } else { + this.existMore = false; + } + notes.forEach(n => (this.$refs.timeline as any).append(n)); + this.moreFetching = false; + }); + }, + onNote(note) { + // Prepend a note + (this.$refs.timeline as any).prepend(note); + }, + onUserAdded() { + this.fetch(); + }, + onUserRemoved() { + this.fetch(); + } + } +}); +</script> diff --git a/src/client/app/mobile/views/components/user-timeline.vue b/src/client/app/mobile/views/components/user-timeline.vue index 40b3be035e628b2ddf46c3a0039e7d3f2e251f44..89ac4d2c6663acd5f03bc5001e2360978753dc21 100644 --- a/src/client/app/mobile/views/components/user-timeline.vue +++ b/src/client/app/mobile/views/components/user-timeline.vue @@ -1,17 +1,10 @@ <template> <div class="mk-user-timeline"> - <mk-notes :notes="notes"> - <div class="init" v-if="fetching"> - %fa:spinner .pulse%%i18n:common.loading% - </div> - <div class="empty" v-if="!fetching && notes.length == 0"> + <mk-notes ref="timeline" :more="existMore ? more : null"> + <div slot="empty"> %fa:R comments% {{ withMedia ? '%i18n:!@no-notes-with-media%' : '%i18n:!@no-notes%' }} </div> - <button v-if="!fetching && existMore" @click="more" :disabled="moreFetching" slot="tail"> - <span v-if="!moreFetching">%i18n:@load-more%</span> - <span v-if="moreFetching">%i18n:common.loading%<mk-ellipsis/></span> - </button> </mk-notes> </div> </template> @@ -19,49 +12,53 @@ <script lang="ts"> import Vue from 'vue'; -const limit = 10; +const fetchLimit = 10; export default Vue.extend({ props: ['user', 'withMedia'], data() { return { fetching: true, - notes: [], existMore: false, moreFetching: false }; }, mounted() { - (this as any).api('users/notes', { - userId: this.user.id, - withMedia: this.withMedia, - limit: limit + 1 - }).then(notes => { - if (notes.length == limit + 1) { - notes.pop(); - this.existMore = true; - } - this.notes = notes; - this.fetching = false; - this.$emit('loaded'); - }); + this.fetch(); }, methods: { + fetch() { + this.fetching = true; + (this.$refs.timeline as any).init(() => new Promise((res, rej) => { + (this as any).api('users/notes', { + userId: this.user.id, + withMedia: this.withMedia, + limit: fetchLimit + 1 + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + this.existMore = true; + } + res(notes); + this.fetching = false; + this.$emit('loaded'); + }, rej); + })); + }, more() { this.moreFetching = true; (this as any).api('users/notes', { userId: this.user.id, withMedia: this.withMedia, - limit: limit + 1, - untilId: this.notes[this.notes.length - 1].id + limit: fetchLimit + 1, + untilId: (this.$refs.timeline as any).tail().id }).then(notes => { - if (notes.length == limit + 1) { + if (notes.length == fetchLimit + 1) { notes.pop(); - this.existMore = true; } else { this.existMore = false; } - this.notes = this.notes.concat(notes); + notes.forEach(n => (this.$refs.timeline as any).append(n)); this.moreFetching = false; }); } diff --git a/src/client/app/mobile/views/components/users-list.vue b/src/client/app/mobile/views/components/users-list.vue index 8fa7a9cbe641354cbadb8952d1265de536db8072..67a38a8955ab12fcd3809de98f9a0b073838e874 100644 --- a/src/client/app/mobile/views/components/users-list.vue +++ b/src/client/app/mobile/views/components/users-list.vue @@ -1,8 +1,8 @@ <template> <div class="mk-users-list"> <nav> - <span :data-is-active="mode == 'all'" @click="mode = 'all'">%i18n:@all%<span>{{ count }}</span></span> - <span v-if="os.isSignedIn && youKnowCount" :data-is-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:@known%<span>{{ youKnowCount }}</span></span> + <span :data-active="mode == 'all'" @click="mode = 'all'">%i18n:@all%<span>{{ count }}</span></span> + <span v-if="os.isSignedIn && youKnowCount" :data-active="mode == 'iknow'" @click="mode = 'iknow'">%i18n:@known%<span>{{ youKnowCount }}</span></span> </nav> <div class="users" v-if="!fetching && users.length != 0"> <mk-user-preview v-for="u in users" :user="u" :key="u.id"/> @@ -85,7 +85,7 @@ export default Vue.extend({ color #657786 border-bottom solid 2px transparent - &[data-is-active] + &[data-active] font-weight bold color $theme-color border-color $theme-color diff --git a/src/client/app/mobile/views/pages/dashboard.vue b/src/client/app/mobile/views/pages/dashboard.vue new file mode 100644 index 0000000000000000000000000000000000000000..14779da650f5bda7c9a9c8aa63a0ae2e92354072 --- /dev/null +++ b/src/client/app/mobile/views/pages/dashboard.vue @@ -0,0 +1,196 @@ +<template> +<mk-ui> + <span slot="header">%fa:home%ダッシュボード</span> + <template slot="func"> + <button @click="customizing = !customizing">%fa:cog%</button> + </template> + <main> + <template v-if="customizing"> + <header> + <select v-model="widgetAdderSelected"> + <option value="profile">プãƒãƒ•ã‚£ãƒ¼ãƒ«</option> + <option value="calendar">カレンダー</option> + <option value="activity">アクティビティ</option> + <option value="rss">RSSリーダー</option> + <option value="photo-stream">フォトストリーム</option> + <option value="slideshow">スライドショー</option> + <option value="version">ãƒãƒ¼ã‚¸ãƒ§ãƒ³</option> + <option value="access-log">アクセスãƒã‚°</option> + <option value="server">サーãƒãƒ¼æƒ…å ±</option> + <option value="donation">寄付ã®ãŠé¡˜ã„</option> + <option value="nav">ナビゲーション</option> + <option value="tips">ヒント</option> + </select> + <button @click="addWidget">è¿½åŠ </button> + <p><a @click="hint">カスタマイズã®ãƒ’ント</a></p> + </header> + <x-draggable + :list="widgets" + :options="{ handle: '.handle', animation: 150 }" + @sort="onWidgetSort" + > + <div v-for="widget in widgets" class="customize-container" :key="widget.id"> + <header> + <span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button> + </header> + <div @click="widgetFunc(widget.id)"> + <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :is-mobile="true"/> + </div> + </div> + </x-draggable> + </template> + <template v-else> + <component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :is-mobile="true" @chosen="warp"/> + </template> + </main> +</mk-ui> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import * as XDraggable from 'vuedraggable'; +import * as uuid from 'uuid'; + +export default Vue.extend({ + components: { + XDraggable + }, + data() { + return { + showNav: false, + widgets: [], + customizing: false, + widgetAdderSelected: null + }; + }, + created() { + if ((this as any).os.i.clientSettings.mobileHome == null) { + Vue.set((this as any).os.i.clientSettings, 'mobileHome', [{ + name: 'calendar', + id: 'a', data: {} + }, { + name: 'activity', + id: 'b', data: {} + }, { + name: 'rss', + id: 'c', data: {} + }, { + name: 'photo-stream', + id: 'd', data: {} + }, { + name: 'donation', + id: 'e', data: {} + }, { + name: 'nav', + id: 'f', data: {} + }, { + name: 'version', + id: 'g', data: {} + }]); + this.widgets = (this as any).os.i.clientSettings.mobileHome; + this.saveHome(); + } else { + this.widgets = (this as any).os.i.clientSettings.mobileHome; + } + + this.$watch('os.i.clientSettings', i => { + this.widgets = (this as any).os.i.clientSettings.mobileHome; + }, { + deep: true + }); + }, + + mounted() { + document.title = 'Misskey'; + document.documentElement.style.background = '#313a42'; + }, + + methods: { + onHomeUpdated(data) { + if (data.home) { + (this as any).os.i.clientSettings.mobileHome = data.home; + this.widgets = data.home; + } else { + const w = (this as any).os.i.clientSettings.mobileHome.find(w => w.id == data.id); + if (w != null) { + w.data = data.data; + this.$refs[w.id][0].preventSave = true; + this.$refs[w.id][0].props = w.data; + this.widgets = (this as any).os.i.clientSettings.mobileHome; + } + } + }, + hint() { + alert('ã‚¦ã‚£ã‚¸ã‚§ãƒƒãƒˆã‚’è¿½åŠ /削除ã—ãŸã‚Šä¸¦ã¹æ›¿ãˆãŸã‚Šã§ãã¾ã™ã€‚ウィジェットを移動ã™ã‚‹ã«ã¯ã€Œä¸‰ã€ã‚’ドラッグã—ã¾ã™ã€‚ウィジェットを削除ã™ã‚‹ã«ã¯ã€Œxã€ã‚’タップã—ã¾ã™ã€‚ã„ãã¤ã‹ã®ã‚¦ã‚£ã‚¸ã‚§ãƒƒãƒˆã¯ã‚¿ãƒƒãƒ—ã™ã‚‹ã“ã¨ã§è¡¨ç¤ºã‚’変更ã§ãã¾ã™ã€‚'); + }, + widgetFunc(id) { + const w = this.$refs[id][0]; + if (w.func) w.func(); + }, + onWidgetSort() { + this.saveHome(); + }, + addWidget() { + const widget = { + name: this.widgetAdderSelected, + id: uuid(), + data: {} + }; + + this.widgets.unshift(widget); + this.saveHome(); + }, + removeWidget(widget) { + this.widgets = this.widgets.filter(w => w.id != widget.id); + this.saveHome(); + }, + saveHome() { + (this as any).os.i.clientSettings.mobileHome = this.widgets; + (this as any).api('i/update_mobile_home', { + home: this.widgets + }); + } + } +}); +</script> + +<style lang="stylus" scoped> +main + margin 0 auto + max-width 500px + + @media (min-width 500px) + padding 8px + + > header + padding 8px + background #fff + + .widget + margin 8px + + .customize-container + margin 8px + background #fff + + > header + line-height 32px + background #eee + + > .handle + padding 0 8px + + > .remove + position absolute + top 0 + right 0 + padding 0 8px + line-height 32px + + > div + padding 8px + + > * + pointer-events none + +</style> diff --git a/src/client/app/mobile/views/pages/home.timeline.vue b/src/client/app/mobile/views/pages/home.timeline.vue new file mode 100644 index 0000000000000000000000000000000000000000..5f4bd6dcd8e65e10e24d64ad6c95af7022cd3d38 --- /dev/null +++ b/src/client/app/mobile/views/pages/home.timeline.vue @@ -0,0 +1,149 @@ +<template> +<div> + <mk-friends-maker v-if="src == 'home' && alone" style="margin-bottom:8px"/> + + <mk-notes ref="timeline" :more="existMore ? more : null"> + <div slot="empty"> + %fa:R comments% + %i18n:@empty% + </div> + </mk-notes> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; + +const fetchLimit = 10; + +export default Vue.extend({ + props: { + src: { + type: String, + required: true + } + }, + + data() { + return { + fetching: true, + moreFetching: false, + existMore: false, + connection: null, + connectionId: null, + unreadCount: 0, + date: null + }; + }, + + computed: { + alone(): boolean { + return (this as any).os.i.followingCount == 0; + }, + + stream(): any { + return this.src == 'home' + ? (this as any).os.stream + : this.src == 'local' + ? (this as any).os.streams.localTimelineStream + : (this as any).os.streams.globalTimelineStream; + }, + + endpoint(): string { + return this.src == 'home' + ? 'notes/timeline' + : this.src == 'local' + ? 'notes/local-timeline' + : 'notes/global-timeline'; + }, + + canFetchMore(): boolean { + return !this.moreFetching && !this.fetching && this.existMore; + } + }, + + mounted() { + this.connection = this.stream.getConnection(); + this.connectionId = this.stream.use(); + + this.connection.on('note', this.onNote); + if (this.src == 'home') { + this.connection.on('follow', this.onChangeFollowing); + this.connection.on('unfollow', this.onChangeFollowing); + } + + this.fetch(); + }, + + beforeDestroy() { + this.connection.off('note', this.onNote); + if (this.src == 'home') { + this.connection.off('follow', this.onChangeFollowing); + this.connection.off('unfollow', this.onChangeFollowing); + } + this.stream.dispose(this.connectionId); + }, + + methods: { + fetch() { + this.fetching = true; + + (this.$refs.timeline as any).init(() => new Promise((res, rej) => { + (this as any).api(this.endpoint, { + limit: fetchLimit + 1, + untilDate: this.date ? this.date.getTime() : undefined, + includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes, + includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + this.existMore = true; + } + res(notes); + this.fetching = false; + this.$emit('loaded'); + }, rej); + })); + }, + + more() { + if (!this.canFetchMore) return; + + this.moreFetching = true; + + (this as any).api(this.endpoint, { + limit: fetchLimit + 1, + untilId: (this.$refs.timeline as any).tail().id, + includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes, + includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes + }).then(notes => { + if (notes.length == fetchLimit + 1) { + notes.pop(); + } else { + this.existMore = false; + } + notes.forEach(n => (this.$refs.timeline as any).append(n)); + this.moreFetching = false; + }); + }, + + onNote(note) { + // Prepend a note + (this.$refs.timeline as any).prepend(note); + }, + + onChangeFollowing() { + this.fetch(); + }, + + focus() { + (this.$refs.timeline as any).focus(); + }, + + warp(date) { + this.date = date; + this.fetch(); + } + } +}); +</script> diff --git a/src/client/app/mobile/views/pages/home.vue b/src/client/app/mobile/views/pages/home.vue index 3d94dd7ce6d4d058bcf28b668e42c0f281a7e57a..92d34fa83bb0c48e783792761f3e9f83398a786b 100644 --- a/src/client/app/mobile/views/pages/home.vue +++ b/src/client/app/mobile/views/pages/home.vue @@ -1,59 +1,42 @@ <template> <mk-ui> - <span slot="header" @click="showTl = !showTl"> - <template v-if="showTl">%fa:home%%i18n:@timeline%</template> - <template v-else>%fa:home%ウィジェット</template> + <span slot="header" @click="showNav = true"> + <span> + <span v-if="src == 'home'">%fa:home%ホーム</span> + <span v-if="src == 'local'">%fa:R comments%ãƒãƒ¼ã‚«ãƒ«</span> + <span v-if="src == 'global'">%fa:globe%ã‚°ãƒãƒ¼ãƒãƒ«</span> + <span v-if="src.startsWith('list')">%fa:list%{{ list.title }}</span> + </span> <span style="margin-left:8px"> - <template v-if="showTl">%fa:angle-down%</template> + <template v-if="!showNav">%fa:angle-down%</template> <template v-else>%fa:angle-up%</template> </span> </span> + <template slot="func"> - <button @click="fn" v-if="showTl">%fa:pencil-alt%</button> - <button @click="customizing = !customizing" v-else>%fa:cog%</button> + <button @click="fn">%fa:pencil-alt%</button> </template> + <main> - <div class="tl"> - <mk-timeline @loaded="onLoaded" v-show="showTl"/> + <div class="nav" v-if="showNav"> + <div class="bg" @click="showNav = false"></div> + <div class="body"> + <div> + <span :data-active="src == 'home'" @click="src = 'home'">%fa:home% ホーム</span> + <span :data-active="src == 'local'" @click="src = 'local'">%fa:R comments% ãƒãƒ¼ã‚«ãƒ«</span> + <span :data-active="src == 'global'" @click="src = 'global'">%fa:globe% ã‚°ãƒãƒ¼ãƒãƒ«</span> + <template v-if="lists"> + <span v-for="l in lists" :data-active="src == 'list:' + l.id" @click="src = 'list:' + l.id; list = l" :key="l.id">%fa:list% {{ l.title }}</span> + </template> + </div> + </div> </div> - <div class="widgets" v-show="!showTl"> - <template v-if="customizing"> - <header> - <select v-model="widgetAdderSelected"> - <option value="profile">プãƒãƒ•ã‚£ãƒ¼ãƒ«</option> - <option value="calendar">カレンダー</option> - <option value="activity">アクティビティ</option> - <option value="rss">RSSリーダー</option> - <option value="photo-stream">フォトストリーム</option> - <option value="slideshow">スライドショー</option> - <option value="version">ãƒãƒ¼ã‚¸ãƒ§ãƒ³</option> - <option value="access-log">アクセスãƒã‚°</option> - <option value="server">サーãƒãƒ¼æƒ…å ±</option> - <option value="donation">寄付ã®ãŠé¡˜ã„</option> - <option value="nav">ナビゲーション</option> - <option value="tips">ヒント</option> - </select> - <button @click="addWidget">è¿½åŠ </button> - <p><a @click="hint">カスタマイズã®ãƒ’ント</a></p> - </header> - <x-draggable - :list="widgets" - :options="{ handle: '.handle', animation: 150 }" - @sort="onWidgetSort" - > - <div v-for="widget in widgets" class="customize-container" :key="widget.id"> - <header> - <span class="handle">%fa:bars%</span>{{ widget.name }}<button class="remove" @click="removeWidget(widget)">%fa:times%</button> - </header> - <div @click="widgetFunc(widget.id)"> - <component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id" :is-customize-mode="true" :is-mobile="true"/> - </div> - </div> - </x-draggable> - </template> - <template v-else> - <component class="widget" v-for="widget in widgets" :is="`mkw-${widget.name}`" :key="widget.id" :ref="widget.id" :widget="widget" :is-mobile="true" @chosen="warp"/> - </template> + + <div class="tl"> + <x-tl v-if="src == 'home'" ref="tl" key="home" src="home" @loaded="onLoaded"/> + <x-tl v-if="src == 'local'" ref="tl" key="local" src="local"/> + <x-tl v-if="src == 'global'" ref="tl" key="global" src="global"/> + <mk-user-list-timeline v-if="src.startsWith('list:')" ref="tl" key="list" :list="list"/> </div> </main> </mk-ui> @@ -61,144 +44,53 @@ <script lang="ts"> import Vue from 'vue'; -import * as XDraggable from 'vuedraggable'; -import * as uuid from 'uuid'; import Progress from '../../../common/scripts/loading'; -import getNoteSummary from '../../../../../renderers/get-note-summary'; +import XTl from './home.timeline.vue'; export default Vue.extend({ components: { - XDraggable + XTl }, + data() { return { - connection: null, - connectionId: null, - unreadCount: 0, - showTl: true, - widgets: [], - customizing: false, - widgetAdderSelected: null + src: 'home', + list: null, + lists: null, + showNav: false }; }, - created() { - if ((this as any).os.i.clientSettings.mobileHome == null) { - Vue.set((this as any).os.i.clientSettings, 'mobileHome', [{ - name: 'calendar', - id: 'a', data: {} - }, { - name: 'activity', - id: 'b', data: {} - }, { - name: 'rss', - id: 'c', data: {} - }, { - name: 'photo-stream', - id: 'd', data: {} - }, { - name: 'donation', - id: 'e', data: {} - }, { - name: 'nav', - id: 'f', data: {} - }, { - name: 'version', - id: 'g', data: {} - }]); - this.widgets = (this as any).os.i.clientSettings.mobileHome; - this.saveHome(); - } else { - this.widgets = (this as any).os.i.clientSettings.mobileHome; - } - this.$watch('os.i.clientSettings', i => { - this.widgets = (this as any).os.i.clientSettings.mobileHome; - }, { - deep: true - }); + watch: { + src() { + this.showNav = false; + }, + + showNav(v) { + if (v && this.lists === null) { + (this as any).api('users/lists/list').then(lists => { + this.lists = lists; + }); + } + } }, + mounted() { document.title = 'Misskey'; document.documentElement.style.background = '#313a42'; - this.connection = (this as any).os.stream.getConnection(); - this.connectionId = (this as any).os.stream.use(); - - this.connection.on('note', this.onStreamNote); - this.connection.on('mobile_home_updated', this.onHomeUpdated); - document.addEventListener('visibilitychange', this.onVisibilitychange, false); - Progress.start(); }, - beforeDestroy() { - this.connection.off('note', this.onStreamNote); - this.connection.off('mobile_home_updated', this.onHomeUpdated); - (this as any).os.stream.dispose(this.connectionId); - document.removeEventListener('visibilitychange', this.onVisibilitychange); - }, + methods: { fn() { (this as any).apis.post(); }, + onLoaded() { Progress.done(); }, - onStreamNote(note) { - if (document.hidden && note.userId !== (this as any).os.i.id) { - this.unreadCount++; - document.title = `(${this.unreadCount}) ${getNoteSummary(note)}`; - } - }, - onVisibilitychange() { - if (!document.hidden) { - this.unreadCount = 0; - document.title = 'Misskey'; - } - }, - onHomeUpdated(data) { - if (data.home) { - (this as any).os.i.clientSettings.mobileHome = data.home; - this.widgets = data.home; - } else { - const w = (this as any).os.i.clientSettings.mobileHome.find(w => w.id == data.id); - if (w != null) { - w.data = data.data; - this.$refs[w.id][0].preventSave = true; - this.$refs[w.id][0].props = w.data; - this.widgets = (this as any).os.i.clientSettings.mobileHome; - } - } - }, - hint() { - alert('ã‚¦ã‚£ã‚¸ã‚§ãƒƒãƒˆã‚’è¿½åŠ /削除ã—ãŸã‚Šä¸¦ã¹æ›¿ãˆãŸã‚Šã§ãã¾ã™ã€‚ウィジェットを移動ã™ã‚‹ã«ã¯ã€Œä¸‰ã€ã‚’ドラッグã—ã¾ã™ã€‚ウィジェットを削除ã™ã‚‹ã«ã¯ã€Œxã€ã‚’タップã—ã¾ã™ã€‚ã„ãã¤ã‹ã®ã‚¦ã‚£ã‚¸ã‚§ãƒƒãƒˆã¯ã‚¿ãƒƒãƒ—ã™ã‚‹ã“ã¨ã§è¡¨ç¤ºã‚’変更ã§ãã¾ã™ã€‚'); - }, - widgetFunc(id) { - const w = this.$refs[id][0]; - if (w.func) w.func(); - }, - onWidgetSort() { - this.saveHome(); - }, - addWidget() { - const widget = { - name: this.widgetAdderSelected, - id: uuid(), - data: {} - }; - this.widgets.unshift(widget); - this.saveHome(); - }, - removeWidget(widget) { - this.widgets = this.widgets.filter(w => w.id != widget.id); - this.saveHome(); - }, - saveHome() { - (this as any).os.i.clientSettings.mobileHome = this.widgets; - (this as any).api('i/update_mobile_home', { - home: this.widgets - }); - }, warp() { } @@ -207,53 +99,75 @@ export default Vue.extend({ </script> <style lang="stylus" scoped> -main +@import '~const.styl' - > .tl - > .mk-timeline - max-width 600px +main + > .nav + > .bg + position fixed + z-index 10000 + top 0 + left 0 + width 100% + height 100% + background rgba(#000, 0.5) + + > .body + position fixed + z-index 10001 + top 56px + left 0 + right 0 + width 300px margin 0 auto - padding 8px - - @media (min-width 500px) - padding 16px - - > .widgets - margin 0 auto - max-width 500px - - @media (min-width 500px) - padding 8px - - > header - padding 8px background #fff + border-radius 8px + box-shadow 0 0 16px rgba(0, 0, 0, 0.1) + + $balloon-size = 16px + + &:before + content "" + display block + position absolute + top -($balloon-size * 2) + 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 $border-color + + &: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 #fff - .widget - margin 8px - - .customize-container - margin 8px - background #fff + > div + padding 8px 0 - > header - line-height 32px - background #eee + > * + display block + padding 8px 16px - > .handle - padding 0 8px + &[data-active] + color $theme-color-foreground + background $theme-color - > .remove - position absolute - top 0 - right 0 - padding 0 8px - line-height 32px + &:not([data-active]):hover + background #eee - > div - padding 8px + > .tl + max-width 600px + margin 0 auto + padding 8px - > * - pointer-events none + @media (min-width 500px) + padding 16px </style> diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue index 3ff9057f73b01eb28c67ca90bf8ad7916332709c..73b8e243156c8f39bebbdf344666ee4238806d1c 100644 --- a/src/client/app/mobile/views/pages/user.vue +++ b/src/client/app/mobile/views/pages/user.vue @@ -45,9 +45,9 @@ </header> <nav> <div class="nav-container"> - <a :data-is-active="page == 'home'" @click="page = 'home'">%i18n:@overview%</a> - <a :data-is-active="page == 'notes'" @click="page = 'notes'">%i18n:@timeline%</a> - <a :data-is-active="page == 'media'" @click="page = 'media'">%i18n:@media%</a> + <a :data-active="page == 'home'" @click="page = 'home'">%i18n:@overview%</a> + <a :data-active="page == 'notes'" @click="page = 'notes'">%i18n:@timeline%</a> + <a :data-active="page == 'media'" @click="page = 'media'">%i18n:@media%</a> </div> </nav> <div class="body"> @@ -256,7 +256,7 @@ main color #657786 border-bottom solid 2px transparent - &[data-is-active] + &[data-active] font-weight bold color $theme-color border-color $theme-color diff --git a/src/models/note-reaction.ts b/src/models/note-reaction.ts index 7891ebdf1719b61fb636472d8dba536f37b188fa..f78b0d9d01f6d0aad9d61c31ad8415e232ff24ee 100644 --- a/src/models/note-reaction.ts +++ b/src/models/note-reaction.ts @@ -1,5 +1,5 @@ import * as mongo from 'mongodb'; -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import deepcopy = require('deepcopy'); import db from '../db/mongodb'; import Reaction from './note-reaction'; diff --git a/src/models/sw-subscription.ts b/src/models/sw-subscription.ts index 621ac8a9b6dea5f1be14e6cdbe3e02239aa94a54..a38edd3a50539ee584bc459d1938276e5d0b7e63 100644 --- a/src/models/sw-subscription.ts +++ b/src/models/sw-subscription.ts @@ -38,4 +38,3 @@ export async function deleteSwSubscription(swSubscription: string | mongo.Object _id: s._id }); } - diff --git a/src/models/user-list.ts b/src/models/user-list.ts new file mode 100644 index 0000000000000000000000000000000000000000..7100fced7e56b154e1217c803a650b790cc4ab26 --- /dev/null +++ b/src/models/user-list.ts @@ -0,0 +1,67 @@ +import * as mongo from 'mongodb'; +import deepcopy = require('deepcopy'); +import db from '../db/mongodb'; + +const UserList = db.get<IUserList>('userList'); +export default UserList; + +export interface IUserList { + _id: mongo.ObjectID; + createdAt: Date; + title: string; + userId: mongo.ObjectID; + userIds: mongo.ObjectID[]; +} + +/** + * UserListを物ç†å‰Šé™¤ã—ã¾ã™ + */ +export async function deleteUserList(userList: string | mongo.ObjectID | IUserList) { + let u: IUserList; + + // Populate + if (mongo.ObjectID.prototype.isPrototypeOf(userList)) { + u = await UserList.findOne({ + _id: userList + }); + } else if (typeof userList === 'string') { + u = await UserList.findOne({ + _id: new mongo.ObjectID(userList) + }); + } else { + u = userList as IUserList; + } + + if (u == null) return; + + // ã“ã®UserListを削除 + await UserList.remove({ + _id: u._id + }); +} + +export const pack = ( + userList: string | mongo.ObjectID | IUserList +) => new Promise<any>(async (resolve, reject) => { + let _userList: any; + + if (mongo.ObjectID.prototype.isPrototypeOf(userList)) { + _userList = await UserList.findOne({ + _id: userList + }); + } else if (typeof userList === 'string') { + _userList = await UserList.findOne({ + _id: new mongo.ObjectID(userList) + }); + } else { + _userList = deepcopy(userList); + } + + if (!_userList) throw `invalid userList arg ${userList}`; + + // Rename _id to id + _userList.id = _userList._id; + delete _userList._id; + + resolve(_userList); +}); diff --git a/src/models/user.ts b/src/models/user.ts index ca1ca289373cbb575de7892ae317d339ab9d534e..0621b6e736dd3f28283351a770240895b89b7e4f 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -1,5 +1,6 @@ import * as mongo from 'mongodb'; import deepcopy = require('deepcopy'); +import sequential = require('promise-sequential'); import rap from '@prezzemolo/rap'; import db from '../db/mongodb'; import Note, { pack as packNote, deleteNote } from './note'; @@ -20,6 +21,7 @@ import FollowingLog, { deleteFollowingLog } from './following-log'; import FollowedLog, { deleteFollowedLog } from './followed-log'; import SwSubscription, { deleteSwSubscription } from './sw-subscription'; import Notification, { deleteNotification } from './notification'; +import UserList, { deleteUserList } from './user-list'; const User = db.get<IUser>('users'); @@ -166,9 +168,9 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) { ).map(x => deleteAccessToken(x))); // ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®Noteã‚’ã™ã¹ã¦å‰Šé™¤ - await Promise.all(( - await Note.find({ userId: u._id }) - ).map(x => deleteNote(x))); + //await sequential(( + // await Note.find({ userId: u._id }) + //).map(x => () => deleteNote(x))); // ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®NoteReactionã‚’ã™ã¹ã¦å‰Šé™¤ await Promise.all(( @@ -260,6 +262,20 @@ export async function deleteUser(user: string | mongo.ObjectID | IUser) { await Notification.find({ notifierId: u._id }) ).map(x => deleteNotification(x))); + // ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã®UserListã‚’ã™ã¹ã¦å‰Šé™¤ + await Promise.all(( + await UserList.find({ userId: u._id }) + ).map(x => deleteUserList(x))); + + // ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒå…¥ã£ã¦ã„ã‚‹ã™ã¹ã¦ã®UserListã‹ã‚‰ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’削除 + await Promise.all(( + await UserList.find({ userIds: u._id }) + ).map(x => + UserList.update({ _id: x._id }, { + $pull: { userIds: u._id } + }) + )); + // ã“ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‚’削除 await User.remove({ _id: u._id diff --git a/src/publishers/stream.ts b/src/publishers/stream.ts index 2ecbfa0dd8f802f7907c23c139e7927441b4b08a..dcc03e39f1636dfca92cd00dc07f11e1f80dfb85 100644 --- a/src/publishers/stream.ts +++ b/src/publishers/stream.ts @@ -25,6 +25,10 @@ class MisskeyEvent { this.publish(`note-stream:${noteId}`, type, typeof value === 'undefined' ? null : value); } + public publishUserListStream(listId: ID, type: string, value?: any): void { + this.publish(`user-list-stream:${listId}`, type, typeof value === 'undefined' ? null : value); + } + public publishMessagingStream(userId: ID, otherpartyId: ID, type: string, value?: any): void { this.publish(`messaging-stream:${userId}-${otherpartyId}`, type, typeof value === 'undefined' ? null : value); } @@ -69,6 +73,7 @@ export default ev.publishUserStream.bind(ev); export const publishLocalTimelineStream = ev.publishLocalTimelineStream.bind(ev); export const publishGlobalTimelineStream = ev.publishGlobalTimelineStream.bind(ev); export const publishDriveStream = ev.publishDriveStream.bind(ev); +export const publishUserListStream = ev.publishUserListStream.bind(ev); export const publishNoteStream = ev.publishNoteStream.bind(ev); export const publishMessagingStream = ev.publishMessagingStream.bind(ev); export const publishMessagingIndexStream = ev.publishMessagingIndexStream.bind(ev); diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts index 3686918147834a504c0392a8ba4ecfa9b9ef3fa9..734b8273f111f3ab20e5dba0a433c0ccbfb918b2 100644 --- a/src/server/api/endpoints.ts +++ b/src/server/api/endpoints.ts @@ -414,6 +414,27 @@ const endpoints: Endpoint[] = [ name: 'users/get_frequently_replied_users' }, + { + name: 'users/lists/show', + withCredential: true, + kind: 'account-read' + }, + { + name: 'users/lists/create', + withCredential: true, + kind: 'account-write' + }, + { + name: 'users/lists/push', + withCredential: true, + kind: 'account-write' + }, + { + name: 'users/lists/list', + withCredential: true, + kind: 'account-read' + }, + { name: 'following/create', withCredential: true, @@ -503,6 +524,14 @@ const endpoints: Endpoint[] = [ max: 100 } }, + { + name: 'notes/user-list-timeline', + withCredential: true, + limit: { + duration: ms('10minutes'), + max: 100 + } + }, { name: 'notes/mentions', withCredential: true, diff --git a/src/server/api/endpoints/aggregation/posts.ts b/src/server/api/endpoints/aggregation/posts.ts index cc2a48b53de3cc011935b36c57249bb1d6fab831..17bead2808c7527df34a1315dc4dc7f596e35bcb 100644 --- a/src/server/api/endpoints/aggregation/posts.ts +++ b/src/server/api/endpoints/aggregation/posts.ts @@ -6,9 +6,6 @@ import Note from '../../../../models/note'; /** * Aggregate notes - * - * @param {any} params - * @return {Promise<any>} */ module.exports = params => new Promise(async (res, rej) => { // Get 'limit' parameter diff --git a/src/server/api/endpoints/aggregation/users.ts b/src/server/api/endpoints/aggregation/users.ts index 19776ed297d99c97d0b3c355adbced3085a36baf..b0a7632f249e33d00fe9c63812ec2c6cc7b75f9a 100644 --- a/src/server/api/endpoints/aggregation/users.ts +++ b/src/server/api/endpoints/aggregation/users.ts @@ -6,9 +6,6 @@ import User from '../../../../models/user'; /** * Aggregate users - * - * @param {any} params - * @return {Promise<any>} */ module.exports = params => new Promise(async (res, rej) => { // Get 'limit' parameter diff --git a/src/server/api/endpoints/aggregation/users/activity.ts b/src/server/api/endpoints/aggregation/users/activity.ts index 318cce77a5b93055f24e0247d21315f6355fdf55..d36e07a4415def5580e4beb6b04ae892b47a0e2f 100644 --- a/src/server/api/endpoints/aggregation/users/activity.ts +++ b/src/server/api/endpoints/aggregation/users/activity.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../../cafy-id'; import User from '../../../../../models/user'; import Note from '../../../../../models/note'; @@ -9,9 +9,6 @@ import Note from '../../../../../models/note'; /** * Aggregate activity of a user - * - * @param {any} params - * @return {Promise<any>} */ module.exports = (params) => new Promise(async (res, rej) => { // Get 'limit' parameter @@ -19,7 +16,7 @@ module.exports = (params) => new Promise(async (res, rej) => { if (limitErr) return rej('invalid limit param'); // Get 'userId' parameter - const [userId, userIdErr] = $(params.userId).id().$; + const [userId, userIdErr] = $(params.userId).type(ID).$; if (userIdErr) return rej('invalid userId param'); // Lookup user diff --git a/src/server/api/endpoints/aggregation/users/followers.ts b/src/server/api/endpoints/aggregation/users/followers.ts index 7ccb2a30661bdb6ee00a6705110890e3b69ef659..a6dd29e735f9c6d196b7cad93de34ef65edf5b50 100644 --- a/src/server/api/endpoints/aggregation/users/followers.ts +++ b/src/server/api/endpoints/aggregation/users/followers.ts @@ -1,19 +1,16 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../../cafy-id'; import User from '../../../../../models/user'; import FollowedLog from '../../../../../models/followed-log'; /** * Aggregate followers of a user - * - * @param {any} params - * @return {Promise<any>} */ module.exports = (params) => new Promise(async (res, rej) => { // Get 'userId' parameter - const [userId, userIdErr] = $(params.userId).id().$; + const [userId, userIdErr] = $(params.userId).type(ID).$; if (userIdErr) return rej('invalid userId param'); // Lookup user diff --git a/src/server/api/endpoints/aggregation/users/following.ts b/src/server/api/endpoints/aggregation/users/following.ts index 45e246495b91856a3d0e85e3a470897ba1851785..7336f392fe7d577d913b2ef580178726d9955008 100644 --- a/src/server/api/endpoints/aggregation/users/following.ts +++ b/src/server/api/endpoints/aggregation/users/following.ts @@ -1,19 +1,16 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../../cafy-id'; import User from '../../../../../models/user'; import FollowingLog from '../../../../../models/following-log'; /** * Aggregate following of a user - * - * @param {any} params - * @return {Promise<any>} */ module.exports = (params) => new Promise(async (res, rej) => { // Get 'userId' parameter - const [userId, userIdErr] = $(params.userId).id().$; + const [userId, userIdErr] = $(params.userId).type(ID).$; if (userIdErr) return rej('invalid userId param'); // Lookup user diff --git a/src/server/api/endpoints/aggregation/users/post.ts b/src/server/api/endpoints/aggregation/users/post.ts index e6170d83e233eca735aeb08b54189ca3d4d718d1..c5a5e5ffcaf04170aad443a83b3610ff6a30a472 100644 --- a/src/server/api/endpoints/aggregation/users/post.ts +++ b/src/server/api/endpoints/aggregation/users/post.ts @@ -1,19 +1,16 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../../cafy-id'; import User from '../../../../../models/user'; import Note from '../../../../../models/note'; /** * Aggregate note of a user - * - * @param {any} params - * @return {Promise<any>} */ module.exports = (params) => new Promise(async (res, rej) => { // Get 'userId' parameter - const [userId, userIdErr] = $(params.userId).id().$; + const [userId, userIdErr] = $(params.userId).type(ID).$; if (userIdErr) return rej('invalid userId param'); // Lookup user diff --git a/src/server/api/endpoints/aggregation/users/reaction.ts b/src/server/api/endpoints/aggregation/users/reaction.ts index 881c7ea6931b1c76fd1b2ff9863b9196a288576c..f1664823cd9196d3249e8d405bfd0df5920eb4c0 100644 --- a/src/server/api/endpoints/aggregation/users/reaction.ts +++ b/src/server/api/endpoints/aggregation/users/reaction.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../../cafy-id'; import User from '../../../../../models/user'; import Reaction from '../../../../../models/note-reaction'; @@ -13,7 +13,7 @@ import Reaction from '../../../../../models/note-reaction'; */ module.exports = (params) => new Promise(async (res, rej) => { // Get 'userId' parameter - const [userId, userIdErr] = $(params.userId).id().$; + const [userId, userIdErr] = $(params.userId).type(ID).$; if (userIdErr) return rej('invalid userId param'); // Lookup user diff --git a/src/server/api/endpoints/app/create.ts b/src/server/api/endpoints/app/create.ts index 4a55d33f2d547399593f53e10db4106ad6b394a0..f403429261006405ea1ae6df9640e120d10d361b 100644 --- a/src/server/api/endpoints/app/create.ts +++ b/src/server/api/endpoints/app/create.ts @@ -79,7 +79,7 @@ module.exports = async (params, user) => new Promise(async (res, rej) => { if (descriptionErr) return rej('invalid description param'); // Get 'permission' parameter - const [permission, permissionErr] = $(params.permission).array('string').unique().$; + const [permission, permissionErr] = $(params.permission).array($().string()).unique().$; if (permissionErr) return rej('invalid permission param'); // Get 'callbackUrl' parameter diff --git a/src/server/api/endpoints/app/show.ts b/src/server/api/endpoints/app/show.ts index 99a2093b6839a6dfdcf695b5a43f8eaa6baea3ec..92a03b9838edb9cef145a415dc409a4192e88dbc 100644 --- a/src/server/api/endpoints/app/show.ts +++ b/src/server/api/endpoints/app/show.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import App, { pack } from '../../../../models/app'; /** @@ -41,7 +41,7 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => { const isSecure = user != null && app == null; // Get 'appId' parameter - const [appId, appIdErr] = $(params.appId).optional.id().$; + const [appId, appIdErr] = $(params.appId).optional.type(ID).$; if (appIdErr) return rej('invalid appId param'); // Get 'nameId' parameter diff --git a/src/server/api/endpoints/channels.ts b/src/server/api/endpoints/channels.ts index 582e6ba43b55737083ad42364eab2163a9af88f9..b68107ed7d93dc942cc7cebfb022633b0512f521 100644 --- a/src/server/api/endpoints/channels.ts +++ b/src/server/api/endpoints/channels.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../cafy-id'; import Channel, { pack } from '../../../models/channel'; /** @@ -17,11 +17,11 @@ module.exports = (params, me) => new Promise(async (res, rej) => { if (limitErr) return rej('invalid limit param'); // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$; if (sinceIdErr) return rej('invalid sinceId param'); // Get 'untilId' parameter - const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$; if (untilIdErr) return rej('invalid untilId param'); // Check if both of sinceId and untilId is specified diff --git a/src/server/api/endpoints/channels/create.ts b/src/server/api/endpoints/channels/create.ts index 0f0f558c8a4cee4edf092b6d14a66dcf643fd280..a737fcb152d190708e4bcaec4d4585adc519b3f3 100644 --- a/src/server/api/endpoints/channels/create.ts +++ b/src/server/api/endpoints/channels/create.ts @@ -8,10 +8,6 @@ import { pack } from '../../../../models/channel'; /** * Create a channel - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = async (params, user) => new Promise(async (res, rej) => { // Get 'title' parameter diff --git a/src/server/api/endpoints/channels/notes.ts b/src/server/api/endpoints/channels/notes.ts index d636aa0d10c366a932ba64eb098b69e602bf6723..73a69c6d2a212cf7bf7c98ae9991a896010ae1f4 100644 --- a/src/server/api/endpoints/channels/notes.ts +++ b/src/server/api/endpoints/channels/notes.ts @@ -1,16 +1,12 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import { default as Channel, IChannel } from '../../../../models/channel'; import Note, { pack } from '../../../../models/note'; /** * Show a notes of a channel - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'limit' parameter @@ -18,11 +14,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (limitErr) return rej('invalid limit param'); // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$; if (sinceIdErr) return rej('invalid sinceId param'); // Get 'untilId' parameter - const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$; if (untilIdErr) return rej('invalid untilId param'); // Check if both of sinceId and untilId is specified @@ -31,7 +27,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { } // Get 'channelId' parameter - const [channelId, channelIdErr] = $(params.channelId).id().$; + const [channelId, channelIdErr] = $(params.channelId).type(ID).$; if (channelIdErr) return rej('invalid channelId param'); // Fetch channel diff --git a/src/server/api/endpoints/channels/show.ts b/src/server/api/endpoints/channels/show.ts index 3ce9ce474550caf0bf9981abdd0dae39bc57a13c..3f468937eddb0f39720b692664a08122c20605fe 100644 --- a/src/server/api/endpoints/channels/show.ts +++ b/src/server/api/endpoints/channels/show.ts @@ -1,19 +1,15 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Channel, { IChannel, pack } from '../../../../models/channel'; /** * Show a channel - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'channelId' parameter - const [channelId, channelIdErr] = $(params.channelId).id().$; + const [channelId, channelIdErr] = $(params.channelId).type(ID).$; if (channelIdErr) return rej('invalid channelId param'); // Fetch channel diff --git a/src/server/api/endpoints/channels/unwatch.ts b/src/server/api/endpoints/channels/unwatch.ts index 8220b90b68a272112ff8c59fb8fc35fa2c3d051a..6ada3c9e1b27bc95af09ab53b851cec682779deb 100644 --- a/src/server/api/endpoints/channels/unwatch.ts +++ b/src/server/api/endpoints/channels/unwatch.ts @@ -1,20 +1,16 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Channel from '../../../../models/channel'; import Watching from '../../../../models/channel-watching'; /** * Unwatch a channel - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'channelId' parameter - const [channelId, channelIdErr] = $(params.channelId).id().$; + const [channelId, channelIdErr] = $(params.channelId).type(ID).$; if (channelIdErr) return rej('invalid channelId param'); //#region Fetch channel diff --git a/src/server/api/endpoints/channels/watch.ts b/src/server/api/endpoints/channels/watch.ts index 6906282a546a6841bb497c01ad564dfaa8d8ad87..7880c3465255bbaf1a8a518ebf5f2331d713cb14 100644 --- a/src/server/api/endpoints/channels/watch.ts +++ b/src/server/api/endpoints/channels/watch.ts @@ -1,20 +1,16 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Channel from '../../../../models/channel'; import Watching from '../../../../models/channel-watching'; /** * Watch a channel - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'channelId' parameter - const [channelId, channelIdErr] = $(params.channelId).id().$; + const [channelId, channelIdErr] = $(params.channelId).type(ID).$; if (channelIdErr) return rej('invalid channelId param'); //#region Fetch channel diff --git a/src/server/api/endpoints/drive/files.ts b/src/server/api/endpoints/drive/files.ts index 63d69d145a35766643961f34f33c58118c0cb11c..7f78ef9daa9507808caddef7feb1a9cab466cb76 100644 --- a/src/server/api/endpoints/drive/files.ts +++ b/src/server/api/endpoints/drive/files.ts @@ -1,16 +1,11 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import DriveFile, { pack } from '../../../../models/drive-file'; /** * Get drive files - * - * @param {any} params - * @param {any} user - * @param {any} app - * @return {Promise<any>} */ module.exports = async (params, user, app) => { // Get 'limit' parameter @@ -18,11 +13,11 @@ module.exports = async (params, user, app) => { if (limitErr) throw 'invalid limit param'; // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$; if (sinceIdErr) throw 'invalid sinceId param'; // Get 'untilId' parameter - const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$; if (untilIdErr) throw 'invalid untilId param'; // Check if both of sinceId and untilId is specified @@ -31,7 +26,7 @@ module.exports = async (params, user, app) => { } // Get 'folderId' parameter - const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$; + const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.type(ID).$; if (folderIdErr) throw 'invalid folderId param'; // Get 'type' parameter diff --git a/src/server/api/endpoints/drive/files/create.ts b/src/server/api/endpoints/drive/files/create.ts index df0bd0a0d3c2ff9172a7b412450c16ce65599238..3d5048732d11622fcfa1366beeca02f611fe539e 100644 --- a/src/server/api/endpoints/drive/files/create.ts +++ b/src/server/api/endpoints/drive/files/create.ts @@ -1,17 +1,12 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../../cafy-id'; import { validateFileName, pack } from '../../../../../models/drive-file'; import create from '../../../../../services/drive/add-file'; /** * Create a file - * - * @param {any} file - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = async (file, params, user): Promise<any> => { if (file == null) { @@ -34,7 +29,7 @@ module.exports = async (file, params, user): Promise<any> => { } // Get 'folderId' parameter - const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$; + const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.type(ID).$; if (folderIdErr) throw 'invalid folderId param'; try { diff --git a/src/server/api/endpoints/drive/files/find.ts b/src/server/api/endpoints/drive/files/find.ts index 0ab6e5d3e325ffd52d9180b7c268ff161341935e..5d49577983e1361c221ff6cb5e35b18c01641807 100644 --- a/src/server/api/endpoints/drive/files/find.ts +++ b/src/server/api/endpoints/drive/files/find.ts @@ -1,15 +1,11 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../../cafy-id'; import DriveFile, { pack } from '../../../../../models/drive-file'; /** * Find a file(s) - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'name' parameter @@ -17,7 +13,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (nameErr) return rej('invalid name param'); // Get 'folderId' parameter - const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$; + const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.type(ID).$; if (folderIdErr) return rej('invalid folderId param'); // Issue query diff --git a/src/server/api/endpoints/drive/files/show.ts b/src/server/api/endpoints/drive/files/show.ts index 3398f24541cd7b5772fd3fb1a99b650e6da4c77f..93c3a63031696c13e914e0725d2cbdbe3d52c5bc 100644 --- a/src/server/api/endpoints/drive/files/show.ts +++ b/src/server/api/endpoints/drive/files/show.ts @@ -1,19 +1,15 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../../cafy-id'; import DriveFile, { pack } from '../../../../../models/drive-file'; /** * Show a file - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = async (params, user) => { // Get 'fileId' parameter - const [fileId, fileIdErr] = $(params.fileId).id().$; + const [fileId, fileIdErr] = $(params.fileId).type(ID).$; if (fileIdErr) throw 'invalid fileId param'; // Fetch file diff --git a/src/server/api/endpoints/drive/files/update.ts b/src/server/api/endpoints/drive/files/update.ts index c783ad8b3b78c45f6f65826db3b6f1e3e4d16670..3ac157b5301b2092c60be0f4568896318b366de6 100644 --- a/src/server/api/endpoints/drive/files/update.ts +++ b/src/server/api/endpoints/drive/files/update.ts @@ -1,21 +1,17 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../../cafy-id'; import DriveFolder from '../../../../../models/drive-folder'; import DriveFile, { validateFileName, pack } from '../../../../../models/drive-file'; import { publishDriveStream } from '../../../../../publishers/stream'; /** * Update a file - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'fileId' parameter - const [fileId, fileIdErr] = $(params.fileId).id().$; + const [fileId, fileIdErr] = $(params.fileId).type(ID).$; if (fileIdErr) return rej('invalid fileId param'); // Fetch file @@ -35,7 +31,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (name) file.filename = name; // Get 'folderId' parameter - const [folderId, folderIdErr] = $(params.folderId).optional.nullable.id().$; + const [folderId, folderIdErr] = $(params.folderId).optional.nullable.type(ID).$; if (folderIdErr) return rej('invalid folderId param'); if (folderId !== undefined) { diff --git a/src/server/api/endpoints/drive/files/upload_from_url.ts b/src/server/api/endpoints/drive/files/upload_from_url.ts index 8a426c0efccad06bfdeaff6f19562eeddb6e23d0..cfae1ae19217ceebf6aee126bf124788e22e8494 100644 --- a/src/server/api/endpoints/drive/files/upload_from_url.ts +++ b/src/server/api/endpoints/drive/files/upload_from_url.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../../cafy-id'; import { pack } from '../../../../../models/drive-file'; import uploadFromUrl from '../../../../../services/drive/upload-from-url'; @@ -15,7 +15,7 @@ module.exports = async (params, user): Promise<any> => { if (urlErr) throw 'invalid url param'; // Get 'folderId' parameter - const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$; + const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.type(ID).$; if (folderIdErr) throw 'invalid folderId param'; return pack(await uploadFromUrl(url, user, folderId)); diff --git a/src/server/api/endpoints/drive/folders.ts b/src/server/api/endpoints/drive/folders.ts index 489e47912ebf1b5cbb0c589c4e1cea6083adc7cf..cba33c42866cd6b6869bcc884f0f82913c2e3cf6 100644 --- a/src/server/api/endpoints/drive/folders.ts +++ b/src/server/api/endpoints/drive/folders.ts @@ -1,16 +1,11 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import DriveFolder, { pack } from '../../../../models/drive-folder'; /** * Get drive folders - * - * @param {any} params - * @param {any} user - * @param {any} app - * @return {Promise<any>} */ module.exports = (params, user, app) => new Promise(async (res, rej) => { // Get 'limit' parameter @@ -18,11 +13,11 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => { if (limitErr) return rej('invalid limit param'); // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$; if (sinceIdErr) return rej('invalid sinceId param'); // Get 'untilId' parameter - const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$; if (untilIdErr) return rej('invalid untilId param'); // Check if both of sinceId and untilId is specified @@ -31,7 +26,7 @@ module.exports = (params, user, app) => new Promise(async (res, rej) => { } // Get 'folderId' parameter - const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.id().$; + const [folderId = null, folderIdErr] = $(params.folderId).optional.nullable.type(ID).$; if (folderIdErr) return rej('invalid folderId param'); // Construct query diff --git a/src/server/api/endpoints/drive/folders/create.ts b/src/server/api/endpoints/drive/folders/create.ts index f34d0019d74bcb86d88ac3a5d7fa898fbf576a65..65425537a2aee07c4631f6f89a9cc995efbafa3d 100644 --- a/src/server/api/endpoints/drive/folders/create.ts +++ b/src/server/api/endpoints/drive/folders/create.ts @@ -1,16 +1,12 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../../cafy-id'; import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder'; import { publishDriveStream } from '../../../../../publishers/stream'; /** * Create drive folder - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'name' parameter @@ -18,7 +14,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (nameErr) return rej('invalid name param'); // Get 'parentId' parameter - const [parentId = null, parentIdErr] = $(params.parentId).optional.nullable.id().$; + const [parentId = null, parentIdErr] = $(params.parentId).optional.nullable.type(ID).$; if (parentIdErr) return rej('invalid parentId param'); // If the parent folder is specified diff --git a/src/server/api/endpoints/drive/folders/find.ts b/src/server/api/endpoints/drive/folders/find.ts index 04dc38f87ff1078a426aecca3ae783b33a59586b..d6277f1978a5040e7a35b0642786d6393630aef7 100644 --- a/src/server/api/endpoints/drive/folders/find.ts +++ b/src/server/api/endpoints/drive/folders/find.ts @@ -1,15 +1,11 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../../cafy-id'; import DriveFolder, { pack } from '../../../../../models/drive-folder'; /** * Find a folder(s) - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'name' parameter @@ -17,7 +13,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (nameErr) return rej('invalid name param'); // Get 'parentId' parameter - const [parentId = null, parentIdErr] = $(params.parentId).optional.nullable.id().$; + const [parentId = null, parentIdErr] = $(params.parentId).optional.nullable.type(ID).$; if (parentIdErr) return rej('invalid parentId param'); // Issue query diff --git a/src/server/api/endpoints/drive/folders/show.ts b/src/server/api/endpoints/drive/folders/show.ts index b432f5a50af4212d3f7f08f962d7f48153912d02..c703209fefa4d23c77594c6848999ab730be1bb7 100644 --- a/src/server/api/endpoints/drive/folders/show.ts +++ b/src/server/api/endpoints/drive/folders/show.ts @@ -1,19 +1,15 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../../cafy-id'; import DriveFolder, { pack } from '../../../../../models/drive-folder'; /** * Show a folder - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'folderId' parameter - const [folderId, folderIdErr] = $(params.folderId).id().$; + const [folderId, folderIdErr] = $(params.folderId).type(ID).$; if (folderIdErr) return rej('invalid folderId param'); // Get folder diff --git a/src/server/api/endpoints/drive/folders/update.ts b/src/server/api/endpoints/drive/folders/update.ts index dd7e8f5c8610e7d0c4c87a7d12de7fb0201554e0..d8da67fac82a3e6caed86187d2640ee083b7a6ec 100644 --- a/src/server/api/endpoints/drive/folders/update.ts +++ b/src/server/api/endpoints/drive/folders/update.ts @@ -1,20 +1,16 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../../cafy-id'; import DriveFolder, { isValidFolderName, pack } from '../../../../../models/drive-folder'; import { publishDriveStream } from '../../../../../publishers/stream'; /** * Update a folder - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'folderId' parameter - const [folderId, folderIdErr] = $(params.folderId).id().$; + const [folderId, folderIdErr] = $(params.folderId).type(ID).$; if (folderIdErr) return rej('invalid folderId param'); // Fetch folder @@ -34,7 +30,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (name) folder.name = name; // Get 'parentId' parameter - const [parentId, parentIdErr] = $(params.parentId).optional.nullable.id().$; + const [parentId, parentIdErr] = $(params.parentId).optional.nullable.type(ID).$; if (parentIdErr) return rej('invalid parentId param'); if (parentId !== undefined) { if (parentId === null) { diff --git a/src/server/api/endpoints/drive/stream.ts b/src/server/api/endpoints/drive/stream.ts index 02313aa37beccacb0c035b2c1d97500fa6d6ff87..00d89582b6a90f08a10427a16933fb312994ed5a 100644 --- a/src/server/api/endpoints/drive/stream.ts +++ b/src/server/api/endpoints/drive/stream.ts @@ -1,15 +1,11 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import DriveFile, { pack } from '../../../../models/drive-file'; /** * Get drive stream - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'limit' parameter @@ -17,11 +13,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (limitErr) return rej('invalid limit param'); // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$; if (sinceIdErr) return rej('invalid sinceId param'); // Get 'untilId' parameter - const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$; if (untilIdErr) return rej('invalid untilId param'); // Check if both of sinceId and untilId is specified diff --git a/src/server/api/endpoints/following/create.ts b/src/server/api/endpoints/following/create.ts index 27e5eb31dbd763de346ce686c564fb7e7d71077a..43f902852e4421debc3184328eda3703c64bf19a 100644 --- a/src/server/api/endpoints/following/create.ts +++ b/src/server/api/endpoints/following/create.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import User from '../../../../models/user'; import Following from '../../../../models/following'; import create from '../../../../services/following/create'; @@ -13,7 +13,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const follower = user; // Get 'userId' parameter - const [userId, userIdErr] = $(params.userId).id().$; + const [userId, userIdErr] = $(params.userId).type(ID).$; if (userIdErr) return rej('invalid userId param'); // 自分自身 diff --git a/src/server/api/endpoints/following/delete.ts b/src/server/api/endpoints/following/delete.ts index ca0703ca224bf6c3a2247a33451dd18c56ccbaf3..99722ccf9186f4950dde94f52be2d992221a18a4 100644 --- a/src/server/api/endpoints/following/delete.ts +++ b/src/server/api/endpoints/following/delete.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import User from '../../../../models/user'; import Following from '../../../../models/following'; import deleteFollowing from '../../../../services/following/delete'; @@ -13,7 +13,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const follower = user; // Get 'userId' parameter - const [userId, userIdErr] = $(params.userId).id().$; + const [userId, userIdErr] = $(params.userId).type(ID).$; if (userIdErr) return rej('invalid userId param'); // Check if the followee is yourself diff --git a/src/server/api/endpoints/following/stalk.ts b/src/server/api/endpoints/following/stalk.ts index fc8be4924dce5d2ca83709198d7b8573680dd412..1dfbc4df986c0f53c900ec002d84b3a74fc8c055 100644 --- a/src/server/api/endpoints/following/stalk.ts +++ b/src/server/api/endpoints/following/stalk.ts @@ -1,6 +1,5 @@ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Following from '../../../../models/following'; -import { isLocalUser } from '../../../../models/user'; /** * Stalk a user @@ -9,7 +8,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const follower = user; // Get 'userId' parameter - const [userId, userIdErr] = $(params.userId).id().$; + const [userId, userIdErr] = $(params.userId).type(ID).$; if (userIdErr) return rej('invalid userId param'); // Fetch following diff --git a/src/server/api/endpoints/following/unstalk.ts b/src/server/api/endpoints/following/unstalk.ts index d7593bcd003b42c4916981522ecf300ed95affc7..0d91ffeac80fb4bb0c2c7d7721ce1922378f8e8a 100644 --- a/src/server/api/endpoints/following/unstalk.ts +++ b/src/server/api/endpoints/following/unstalk.ts @@ -1,4 +1,4 @@ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Following from '../../../../models/following'; /** @@ -8,7 +8,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const follower = user; // Get 'userId' parameter - const [userId, userIdErr] = $(params.userId).id().$; + const [userId, userIdErr] = $(params.userId).type(ID).$; if (userIdErr) return rej('invalid userId param'); // Fetch following diff --git a/src/server/api/endpoints/i/authorized_apps.ts b/src/server/api/endpoints/i/authorized_apps.ts index 82fd2d251698d5d3af4c03cd890ecc73b40eb8a4..fd12b3dec02d0f1fbc59aaeece2ffcf908183819 100644 --- a/src/server/api/endpoints/i/authorized_apps.ts +++ b/src/server/api/endpoints/i/authorized_apps.ts @@ -7,10 +7,6 @@ import { pack } from '../../../../models/app'; /** * Get authorized apps of my account - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'limit' parameter diff --git a/src/server/api/endpoints/i/change_password.ts b/src/server/api/endpoints/i/change_password.ts index 57415083f18227facf3276aac3065f8f102c1bd2..a24e9f0be11ffeb6620e6b2ec4ffa844f52790bd 100644 --- a/src/server/api/endpoints/i/change_password.ts +++ b/src/server/api/endpoints/i/change_password.ts @@ -7,10 +7,6 @@ import User from '../../../../models/user'; /** * Change password - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = async (params, user) => new Promise(async (res, rej) => { // Get 'currentPasword' parameter diff --git a/src/server/api/endpoints/i/favorites.ts b/src/server/api/endpoints/i/favorites.ts index f390ef9ec76a91fc1e0fbae3ce988e9409483a50..a2c472ad179ccf38f2aa03d4db52ecd3a4be283c 100644 --- a/src/server/api/endpoints/i/favorites.ts +++ b/src/server/api/endpoints/i/favorites.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Favorite, { pack } from '../../../../models/favorite'; /** @@ -13,11 +13,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (limitErr) return rej('invalid limit param'); // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$; if (sinceIdErr) return rej('invalid sinceId param'); // Get 'untilId' parameter - const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$; if (untilIdErr) return rej('invalid untilId param'); // Check if both of sinceId and untilId is specified diff --git a/src/server/api/endpoints/i/notifications.ts b/src/server/api/endpoints/i/notifications.ts index 69a8910898555d7a362abbac58a2606b582ed3ba..14ade7b023652680cab859f811ddba2de2d203eb 100644 --- a/src/server/api/endpoints/i/notifications.ts +++ b/src/server/api/endpoints/i/notifications.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Notification from '../../../../models/notification'; import Mute from '../../../../models/mute'; import { pack } from '../../../../models/notification'; @@ -22,7 +22,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (markAsReadErr) return rej('invalid markAsRead param'); // Get 'type' parameter - const [type, typeErr] = $(params.type).optional.array('string').unique().$; + const [type, typeErr] = $(params.type).optional.array($().string()).unique().$; if (typeErr) return rej('invalid type param'); // Get 'limit' parameter @@ -30,11 +30,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (limitErr) return rej('invalid limit param'); // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$; if (sinceIdErr) return rej('invalid sinceId param'); // Get 'untilId' parameter - const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$; if (untilIdErr) return rej('invalid untilId param'); // Check if both of sinceId and untilId is specified diff --git a/src/server/api/endpoints/i/pin.ts b/src/server/api/endpoints/i/pin.ts index 909a6fdbdef9e77422d273adf8c24fe6adb32fde..761e41bbeaa636bc67a82bf2e21e1ccabc16c856 100644 --- a/src/server/api/endpoints/i/pin.ts +++ b/src/server/api/endpoints/i/pin.ts @@ -1,21 +1,17 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import User from '../../../../models/user'; import Note from '../../../../models/note'; import { pack } from '../../../../models/user'; /** * Pin note - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = async (params, user) => new Promise(async (res, rej) => { // Get 'noteId' parameter - const [noteId, noteIdErr] = $(params.noteId).id().$; + const [noteId, noteIdErr] = $(params.noteId).type(ID).$; if (noteIdErr) return rej('invalid noteId param'); // Fetch pinee diff --git a/src/server/api/endpoints/i/regenerate_token.ts b/src/server/api/endpoints/i/regenerate_token.ts index f9e92c1797fcc86f6a5c0947a505b8629c4ae2bf..945ddbdee46079a356a55e2725ffb34903b68106 100644 --- a/src/server/api/endpoints/i/regenerate_token.ts +++ b/src/server/api/endpoints/i/regenerate_token.ts @@ -9,10 +9,6 @@ import generateUserToken from '../../common/generate-native-user-token'; /** * Regenerate native token - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = async (params, user) => new Promise(async (res, rej) => { // Get 'password' parameter diff --git a/src/server/api/endpoints/i/signin_history.ts b/src/server/api/endpoints/i/signin_history.ts index 931b9e2252e636d5038ebb802c8dcaaae1fc9924..77beca9fd6d97b55ff10a4251cd4a510379da0c5 100644 --- a/src/server/api/endpoints/i/signin_history.ts +++ b/src/server/api/endpoints/i/signin_history.ts @@ -1,15 +1,11 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Signin, { pack } from '../../../../models/signin'; /** * Get signin history of my account - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'limit' parameter @@ -17,11 +13,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (limitErr) return rej('invalid limit param'); // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$; if (sinceIdErr) return rej('invalid sinceId param'); // Get 'untilId' parameter - const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$; if (untilIdErr) return rej('invalid untilId param'); // Check if both of sinceId and untilId is specified diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts index f3c9d777b5b546de851978af77bbb55ab95c0e26..7505e7338709dc0bf0533657902fc4d821e5ac33 100644 --- a/src/server/api/endpoints/i/update.ts +++ b/src/server/api/endpoints/i/update.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import User, { isValidName, isValidDescription, isValidLocation, isValidBirthday, pack } from '../../../../models/user'; import event from '../../../../publishers/stream'; @@ -32,12 +32,12 @@ module.exports = async (params, user, app) => new Promise(async (res, rej) => { if (birthday !== undefined) user.profile.birthday = birthday; // Get 'avatarId' parameter - const [avatarId, avatarIdErr] = $(params.avatarId).optional.id().$; + const [avatarId, avatarIdErr] = $(params.avatarId).optional.type(ID).$; if (avatarIdErr) return rej('invalid avatarId param'); if (avatarId) user.avatarId = avatarId; // Get 'bannerId' parameter - const [bannerId, bannerIdErr] = $(params.bannerId).optional.id().$; + const [bannerId, bannerIdErr] = $(params.bannerId).optional.type(ID).$; if (bannerIdErr) return rej('invalid bannerId param'); if (bannerId) user.bannerId = bannerId; diff --git a/src/server/api/endpoints/i/update_client_setting.ts b/src/server/api/endpoints/i/update_client_setting.ts index b0d5db5ec2337343167c71bfe6e4afb903d29de5..f753c8bcc412a71c12fcf7e101f48ee7f5302b8e 100644 --- a/src/server/api/endpoints/i/update_client_setting.ts +++ b/src/server/api/endpoints/i/update_client_setting.ts @@ -7,10 +7,6 @@ import event from '../../../../publishers/stream'; /** * Update myself - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = async (params, user) => new Promise(async (res, rej) => { // Get 'name' parameter diff --git a/src/server/api/endpoints/i/update_home.ts b/src/server/api/endpoints/i/update_home.ts index ce7661ede0822de263755451cf776291345452fd..4b8ba25069bff478cb17961215693cf7fc1ca2e7 100644 --- a/src/server/api/endpoints/i/update_home.ts +++ b/src/server/api/endpoints/i/update_home.ts @@ -8,7 +8,7 @@ import event from '../../../../publishers/stream'; module.exports = async (params, user) => new Promise(async (res, rej) => { // Get 'home' parameter const [home, homeErr] = $(params.home).optional.array().each( - $().strict.object() + $().object(true) .have('name', $().string()) .have('id', $().string()) .have('place', $().string()) diff --git a/src/server/api/endpoints/i/update_mobile_home.ts b/src/server/api/endpoints/i/update_mobile_home.ts index b710e2f330e6db1c3c9126045b981796d0dae37a..c3ecea71780ac00f3943ddeed79f43331f8896a9 100644 --- a/src/server/api/endpoints/i/update_mobile_home.ts +++ b/src/server/api/endpoints/i/update_mobile_home.ts @@ -8,7 +8,7 @@ import event from '../../../../publishers/stream'; module.exports = async (params, user) => new Promise(async (res, rej) => { // Get 'home' parameter const [home, homeErr] = $(params.home).optional.array().each( - $().strict.object() + $().object(true) .have('name', $().string()) .have('id', $().string()) .have('data', $().object())).$; diff --git a/src/server/api/endpoints/messaging/history.ts b/src/server/api/endpoints/messaging/history.ts index e42d34f21a9f99eb315b53b01d2b86e3fb16ea14..654bf5c198e66bb7b73a2bb99589f9f6bd7f162a 100644 --- a/src/server/api/endpoints/messaging/history.ts +++ b/src/server/api/endpoints/messaging/history.ts @@ -8,10 +8,6 @@ import { pack } from '../../../../models/messaging-message'; /** * Show messaging history - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'limit' parameter diff --git a/src/server/api/endpoints/messaging/messages.ts b/src/server/api/endpoints/messaging/messages.ts index 092eab0562ce8f8a279b6f384d3de8db29e2b625..f28699cb88a54a62ffd921904f312e9b73293393 100644 --- a/src/server/api/endpoints/messaging/messages.ts +++ b/src/server/api/endpoints/messaging/messages.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Message from '../../../../models/messaging-message'; import User from '../../../../models/user'; import { pack } from '../../../../models/messaging-message'; @@ -16,7 +16,7 @@ import read from '../../common/read-messaging-message'; */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'userId' parameter - const [recipientId, recipientIdErr] = $(params.userId).id().$; + const [recipientId, recipientIdErr] = $(params.userId).type(ID).$; if (recipientIdErr) return rej('invalid userId param'); // Fetch recipient @@ -41,11 +41,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (limitErr) return rej('invalid limit param'); // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$; if (sinceIdErr) return rej('invalid sinceId param'); // Get 'untilId' parameter - const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$; if (untilIdErr) return rej('invalid untilId param'); // Check if both of sinceId and untilId is specified diff --git a/src/server/api/endpoints/messaging/messages/create.ts b/src/server/api/endpoints/messaging/messages/create.ts index 0483b602b299c823e6da1f179de76ff5d2852329..cce326be6eb965ed19d2504bcd25487b4c3593d3 100644 --- a/src/server/api/endpoints/messaging/messages/create.ts +++ b/src/server/api/endpoints/messaging/messages/create.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../../cafy-id'; import Message from '../../../../../models/messaging-message'; import { isValidText } from '../../../../../models/messaging-message'; import History from '../../../../../models/messaging-history'; @@ -16,14 +16,10 @@ import config from '../../../../../config'; /** * Create a message - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'userId' parameter - const [recipientId, recipientIdErr] = $(params.userId).id().$; + const [recipientId, recipientIdErr] = $(params.userId).type(ID).$; if (recipientIdErr) return rej('invalid userId param'); // Myself @@ -49,7 +45,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (textErr) return rej('invalid text'); // Get 'fileId' parameter - const [fileId, fileIdErr] = $(params.fileId).optional.id().$; + const [fileId, fileIdErr] = $(params.fileId).optional.type(ID).$; if (fileIdErr) return rej('invalid fileId param'); let file = null; diff --git a/src/server/api/endpoints/messaging/unread.ts b/src/server/api/endpoints/messaging/unread.ts index 30d59dd8bdb0c9b52d859011ef0df81c2cb6027f..1d83af501d392de1e9c681a3332c2e73a8cb288f 100644 --- a/src/server/api/endpoints/messaging/unread.ts +++ b/src/server/api/endpoints/messaging/unread.ts @@ -6,10 +6,6 @@ import Mute from '../../../../models/mute'; /** * Get count of unread messages - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { const mute = await Mute.find({ diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts index 26ae612cab248d646a9dfb158fe15dafc3cd9f91..0d59ecc118bb49d43bee6f353893f1b79c818746 100644 --- a/src/server/api/endpoints/mute/create.ts +++ b/src/server/api/endpoints/mute/create.ts @@ -1,22 +1,18 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import User from '../../../../models/user'; import Mute from '../../../../models/mute'; /** * Mute a user - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { const muter = user; // Get 'userId' parameter - const [userId, userIdErr] = $(params.userId).id().$; + const [userId, userIdErr] = $(params.userId).type(ID).$; if (userIdErr) return rej('invalid userId param'); // 自分自身 diff --git a/src/server/api/endpoints/mute/delete.ts b/src/server/api/endpoints/mute/delete.ts index 6f617416c8ed121693aec7ad3c308ded4f67074a..3a37de9a214f4074bb1a3b8d22c93be421ebe476 100644 --- a/src/server/api/endpoints/mute/delete.ts +++ b/src/server/api/endpoints/mute/delete.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import User from '../../../../models/user'; import Mute from '../../../../models/mute'; @@ -12,7 +12,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => { const muter = user; // Get 'userId' parameter - const [userId, userIdErr] = $(params.userId).id().$; + const [userId, userIdErr] = $(params.userId).type(ID).$; if (userIdErr) return rej('invalid userId param'); // Check if the mutee is yourself diff --git a/src/server/api/endpoints/mute/list.ts b/src/server/api/endpoints/mute/list.ts index 0b8262d6c59e826619e87afcb067de2e6dc51bb0..f35bf7d168086b3fd64b931765d00e1e7397bf1e 100644 --- a/src/server/api/endpoints/mute/list.ts +++ b/src/server/api/endpoints/mute/list.ts @@ -1,17 +1,13 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Mute from '../../../../models/mute'; import { pack } from '../../../../models/user'; import { getFriendIds } from '../../common/get-friends'; /** * Get muted users of a user - * - * @param {any} params - * @param {any} me - * @return {Promise<any>} */ module.exports = (params, me) => new Promise(async (res, rej) => { // Get 'iknow' parameter @@ -23,7 +19,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { if (limitErr) return rej('invalid limit param'); // Get 'cursor' parameter - const [cursor = null, cursorErr] = $(params.cursor).optional.id().$; + const [cursor = null, cursorErr] = $(params.cursor).optional.type(ID).$; if (cursorErr) return rej('invalid cursor param'); // Construct query diff --git a/src/server/api/endpoints/my/apps.ts b/src/server/api/endpoints/my/apps.ts index 2a3f8bcd7a9e9e560e1db97bfd42ef479667cd0f..eb7ece70e9a1160d0982422034a099c455806822 100644 --- a/src/server/api/endpoints/my/apps.ts +++ b/src/server/api/endpoints/my/apps.ts @@ -6,10 +6,6 @@ import App, { pack } from '../../../../models/app'; /** * Get my apps - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'limit' parameter diff --git a/src/server/api/endpoints/notes.ts b/src/server/api/endpoints/notes.ts index a70ac0588f6acbf4cb2359e819470634b659dd41..bf4d5bc66fa395f63afa2367eddbf4e915ec79a4 100644 --- a/src/server/api/endpoints/notes.ts +++ b/src/server/api/endpoints/notes.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../cafy-id'; import Note, { pack } from '../../../models/note'; /** @@ -33,11 +33,11 @@ module.exports = (params) => new Promise(async (res, rej) => { if (limitErr) return rej('invalid limit param'); // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$; if (sinceIdErr) return rej('invalid sinceId param'); // Get 'untilId' parameter - const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$; if (untilIdErr) return rej('invalid untilId param'); // Check if both of sinceId and untilId is specified diff --git a/src/server/api/endpoints/notes/context.ts b/src/server/api/endpoints/notes/context.ts index 2caf742d26600fe44f719e40f339cece8bbc5cd6..309fc2644760456dc80d505a2f654877444c9e1d 100644 --- a/src/server/api/endpoints/notes/context.ts +++ b/src/server/api/endpoints/notes/context.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Note, { pack } from '../../../../models/note'; /** @@ -13,7 +13,7 @@ import Note, { pack } from '../../../../models/note'; */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'noteId' parameter - const [noteId, noteIdErr] = $(params.noteId).id().$; + const [noteId, noteIdErr] = $(params.noteId).type(ID).$; if (noteIdErr) return rej('invalid noteId param'); // Get 'limit' parameter diff --git a/src/server/api/endpoints/notes/create.ts b/src/server/api/endpoints/notes/create.ts index ea1f41aae2713ecaec50a5352272f3de8967429a..1824a16c24d697895bd6f93bd12f4d67f3bf7bca 100644 --- a/src/server/api/endpoints/notes/create.ts +++ b/src/server/api/endpoints/notes/create.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Note, { INote, isValidText, isValidCw, pack } from '../../../../models/note'; import { ILocalUser } from '../../../../models/user'; import Channel, { IChannel } from '../../../../models/channel'; @@ -11,11 +11,6 @@ import { IApp } from '../../../../models/app'; /** * Create a note - * - * @param {any} params - * @param {any} user - * @param {any} app - * @return {Promise<any>} */ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res, rej) => { // Get 'visibility' parameter @@ -35,11 +30,11 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res if (viaMobileErr) return rej('invalid viaMobile'); // Get 'tags' parameter - const [tags = [], tagsErr] = $(params.tags).optional.array('string').unique().eachQ(t => t.range(1, 32)).$; + const [tags = [], tagsErr] = $(params.tags).optional.array($().string().range(1, 32)).unique().$; if (tagsErr) return rej('invalid tags'); // Get 'geo' parameter - const [geo, geoErr] = $(params.geo).optional.nullable.strict.object() + const [geo, geoErr] = $(params.geo).optional.nullable.object(true) .have('coordinates', $().array().length(2) .item(0, $().number().range(-180, 180)) .item(1, $().number().range(-90, 90))) @@ -52,7 +47,7 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res if (geoErr) return rej('invalid geo'); // Get 'mediaIds' parameter - const [mediaIds, mediaIdsErr] = $(params.mediaIds).optional.array('id').unique().range(1, 4).$; + const [mediaIds, mediaIdsErr] = $(params.mediaIds).optional.array($().type(ID)).unique().range(1, 4).$; if (mediaIdsErr) return rej('invalid mediaIds'); let files = []; @@ -79,7 +74,7 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res } // Get 'renoteId' parameter - const [renoteId, renoteIdErr] = $(params.renoteId).optional.id().$; + const [renoteId, renoteIdErr] = $(params.renoteId).optional.type(ID).$; if (renoteIdErr) return rej('invalid renoteId'); let renote: INote = null; @@ -100,7 +95,7 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res } // Get 'replyId' parameter - const [replyId, replyIdErr] = $(params.replyId).optional.id().$; + const [replyId, replyIdErr] = $(params.replyId).optional.type(ID).$; if (replyIdErr) return rej('invalid replyId'); let reply: INote = null; @@ -121,7 +116,7 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res } // Get 'channelId' parameter - const [channelId, channelIdErr] = $(params.channelId).optional.id().$; + const [channelId, channelIdErr] = $(params.channelId).optional.type(ID).$; if (channelIdErr) return rej('invalid channelId'); let channel: IChannel = null; @@ -162,8 +157,8 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res } // Get 'poll' parameter - const [poll, pollErr] = $(params.poll).optional.strict.object() - .have('choices', $().array('string') + const [poll, pollErr] = $(params.poll).optional.object(true) + .have('choices', $().array($().string()) .unique() .range(2, 10) .each(c => c.length > 0 && c.length < 50)) diff --git a/src/server/api/endpoints/notes/favorites/create.ts b/src/server/api/endpoints/notes/favorites/create.ts index c8e7f524269c1888d02226eea41244578585c4de..e4c4adb9bbb8bc24b38bde49cb5e167927e031e5 100644 --- a/src/server/api/endpoints/notes/favorites/create.ts +++ b/src/server/api/endpoints/notes/favorites/create.ts @@ -1,20 +1,16 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../../cafy-id'; import Favorite from '../../../../../models/favorite'; import Note from '../../../../../models/note'; /** * Favorite a note - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'noteId' parameter - const [noteId, noteIdErr] = $(params.noteId).id().$; + const [noteId, noteIdErr] = $(params.noteId).type(ID).$; if (noteIdErr) return rej('invalid noteId param'); // Get favoritee diff --git a/src/server/api/endpoints/notes/favorites/delete.ts b/src/server/api/endpoints/notes/favorites/delete.ts index 92aceb343b9f0b6c9f6aadd1b4cb64412ccb7b07..3c4d9a11118c91ce98d6d71a1923ad5aa244318b 100644 --- a/src/server/api/endpoints/notes/favorites/delete.ts +++ b/src/server/api/endpoints/notes/favorites/delete.ts @@ -1,20 +1,16 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../../cafy-id'; import Favorite from '../../../../../models/favorite'; import Note from '../../../../../models/note'; /** * Unfavorite a note - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'noteId' parameter - const [noteId, noteIdErr] = $(params.noteId).id().$; + const [noteId, noteIdErr] = $(params.noteId).type(ID).$; if (noteIdErr) return rej('invalid noteId param'); // Get favoritee diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts index 07e138ec54293bd1dae809aeb9fb3c1eff01a8c4..e2a94d8a3eb6421b3fef28b541a1437001a177a7 100644 --- a/src/server/api/endpoints/notes/global-timeline.ts +++ b/src/server/api/endpoints/notes/global-timeline.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Note from '../../../../models/note'; import Mute from '../../../../models/mute'; import { pack } from '../../../../models/note'; @@ -15,11 +15,11 @@ module.exports = async (params, user, app) => { if (limitErr) throw 'invalid limit param'; // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$; if (sinceIdErr) throw 'invalid sinceId param'; // Get 'untilId' parameter - const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$; if (untilIdErr) throw 'invalid untilId param'; // Get 'sinceDate' parameter diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts index d63528c3cdda46ac4ef9d54e978bd84ab50b4e85..dda83311ac23e9dd6d73bb40960f348bec476601 100644 --- a/src/server/api/endpoints/notes/local-timeline.ts +++ b/src/server/api/endpoints/notes/local-timeline.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Note from '../../../../models/note'; import Mute from '../../../../models/mute'; import { pack } from '../../../../models/note'; @@ -15,11 +15,11 @@ module.exports = async (params, user, app) => { if (limitErr) throw 'invalid limit param'; // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$; if (sinceIdErr) throw 'invalid sinceId param'; // Get 'untilId' parameter - const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$; if (untilIdErr) throw 'invalid untilId param'; // Get 'sinceDate' parameter diff --git a/src/server/api/endpoints/notes/mentions.ts b/src/server/api/endpoints/notes/mentions.ts index 2d95606b3f3a310f84bab612dbe82079eab25b58..815cf271a215764ab374556fb210f4dc848e97e5 100644 --- a/src/server/api/endpoints/notes/mentions.ts +++ b/src/server/api/endpoints/notes/mentions.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Note from '../../../../models/note'; import { getFriendIds } from '../../common/get-friends'; import { pack } from '../../../../models/note'; @@ -24,11 +24,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (limitErr) return rej('invalid limit param'); // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$; if (sinceIdErr) return rej('invalid sinceId param'); // Get 'untilId' parameter - const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$; if (untilIdErr) return rej('invalid untilId param'); // Check if both of sinceId and untilId is specified diff --git a/src/server/api/endpoints/notes/polls/recommendation.ts b/src/server/api/endpoints/notes/polls/recommendation.ts index cb530ea2cfb4dc873d0cbc0b249c6a2d1923acf2..24b0a4c803abc57b3fa16138f4c3354036f1cb0b 100644 --- a/src/server/api/endpoints/notes/polls/recommendation.ts +++ b/src/server/api/endpoints/notes/polls/recommendation.ts @@ -7,10 +7,6 @@ import Note, { pack } from '../../../../../models/note'; /** * Get recommended polls - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'limit' parameter diff --git a/src/server/api/endpoints/notes/polls/vote.ts b/src/server/api/endpoints/notes/polls/vote.ts index 03d94da60d3f8ee72c41fc719e12a6f66fdcdd4d..2669c39085ada605147ec8b4fe14e090e81c0852 100644 --- a/src/server/api/endpoints/notes/polls/vote.ts +++ b/src/server/api/endpoints/notes/polls/vote.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../../cafy-id'; import Vote from '../../../../../models/poll-vote'; import Note from '../../../../../models/note'; import Watching from '../../../../../models/note-watching'; @@ -11,14 +11,10 @@ import notify from '../../../../../publishers/notify'; /** * Vote poll of a note - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'noteId' parameter - const [noteId, noteIdErr] = $(params.noteId).id().$; + const [noteId, noteIdErr] = $(params.noteId).type(ID).$; if (noteIdErr) return rej('invalid noteId param'); // Get votee diff --git a/src/server/api/endpoints/notes/reactions.ts b/src/server/api/endpoints/notes/reactions.ts index bbff97bb0a5765f263d05e9254eae4e106bd4c9f..68ffbacd4675c9aa704f37cd12509a51dc97eed0 100644 --- a/src/server/api/endpoints/notes/reactions.ts +++ b/src/server/api/endpoints/notes/reactions.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Note from '../../../../models/note'; import Reaction, { pack } from '../../../../models/note-reaction'; @@ -14,7 +14,7 @@ import Reaction, { pack } from '../../../../models/note-reaction'; */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'noteId' parameter - const [noteId, noteIdErr] = $(params.noteId).id().$; + const [noteId, noteIdErr] = $(params.noteId).type(ID).$; if (noteIdErr) return rej('invalid noteId param'); // Get 'limit' parameter diff --git a/src/server/api/endpoints/notes/reactions/create.ts b/src/server/api/endpoints/notes/reactions/create.ts index 9e217cc3e03062d7c741b016efc9e0c7f0fce9f6..1c212526046ff1df22dcd9688eeb2000628b50d2 100644 --- a/src/server/api/endpoints/notes/reactions/create.ts +++ b/src/server/api/endpoints/notes/reactions/create.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../../cafy-id'; import Note from '../../../../../models/note'; import create from '../../../../../services/note/reaction/create'; import { validateReaction } from '../../../../../models/note-reaction'; @@ -11,7 +11,7 @@ import { validateReaction } from '../../../../../models/note-reaction'; */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'noteId' parameter - const [noteId, noteIdErr] = $(params.noteId).id().$; + const [noteId, noteIdErr] = $(params.noteId).type(ID).$; if (noteIdErr) return rej('invalid noteId param'); // Get 'reaction' parameter diff --git a/src/server/api/endpoints/notes/reactions/delete.ts b/src/server/api/endpoints/notes/reactions/delete.ts index b5d738b8ffeff00386b80b511caa787de70ba0ec..be3c1b214dd52b7dbca135692dc74640b635ac64 100644 --- a/src/server/api/endpoints/notes/reactions/delete.ts +++ b/src/server/api/endpoints/notes/reactions/delete.ts @@ -1,21 +1,16 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../../cafy-id'; import Reaction from '../../../../../models/note-reaction'; import Note from '../../../../../models/note'; -// import event from '../../../publishers/stream'; /** * Unreact to a note - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'noteId' parameter - const [noteId, noteIdErr] = $(params.noteId).id().$; + const [noteId, noteIdErr] = $(params.noteId).type(ID).$; if (noteIdErr) return rej('invalid noteId param'); // Fetch unreactee diff --git a/src/server/api/endpoints/notes/replies.ts b/src/server/api/endpoints/notes/replies.ts index 88d9ff329a74b3f9efa07727d4eb081fa7498e4b..31f1bb941aea046a14c47670065129c201f977ea 100644 --- a/src/server/api/endpoints/notes/replies.ts +++ b/src/server/api/endpoints/notes/replies.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Note, { pack } from '../../../../models/note'; /** @@ -13,7 +13,7 @@ import Note, { pack } from '../../../../models/note'; */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'noteId' parameter - const [noteId, noteIdErr] = $(params.noteId).id().$; + const [noteId, noteIdErr] = $(params.noteId).type(ID).$; if (noteIdErr) return rej('invalid noteId param'); // Get 'limit' parameter diff --git a/src/server/api/endpoints/notes/reposts.ts b/src/server/api/endpoints/notes/reposts.ts index 9dfc2c3cb5c3dcbb934ca6a0eddb45857cd5e337..fe989313804312e98230b776cc6f3b0e7c0ab23f 100644 --- a/src/server/api/endpoints/notes/reposts.ts +++ b/src/server/api/endpoints/notes/reposts.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Note, { pack } from '../../../../models/note'; /** @@ -13,7 +13,7 @@ import Note, { pack } from '../../../../models/note'; */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'noteId' parameter - const [noteId, noteIdErr] = $(params.noteId).id().$; + const [noteId, noteIdErr] = $(params.noteId).type(ID).$; if (noteIdErr) return rej('invalid noteId param'); // Get 'limit' parameter @@ -21,11 +21,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (limitErr) return rej('invalid limit param'); // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$; if (sinceIdErr) return rej('invalid sinceId param'); // Get 'untilId' parameter - const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$; if (untilIdErr) return rej('invalid untilId param'); // Check if both of sinceId and untilId is specified diff --git a/src/server/api/endpoints/notes/search.ts b/src/server/api/endpoints/notes/search.ts index 3ff3fbbafa8cdd5677a62dcbcc41ef97a603f9ea..021f620aa294b0dc0023e9dc0a3a5c169b5735d3 100644 --- a/src/server/api/endpoints/notes/search.ts +++ b/src/server/api/endpoints/notes/search.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; const escapeRegexp = require('escape-regexp'); import Note from '../../../../models/note'; import User from '../../../../models/user'; @@ -22,19 +22,19 @@ module.exports = (params, me) => new Promise(async (res, rej) => { if (textError) return rej('invalid text param'); // Get 'includeUserIds' parameter - const [includeUserIds = [], includeUserIdsErr] = $(params.includeUserIds).optional.array('id').$; + const [includeUserIds = [], includeUserIdsErr] = $(params.includeUserIds).optional.array($().type(ID)).$; if (includeUserIdsErr) return rej('invalid includeUserIds param'); // Get 'excludeUserIds' parameter - const [excludeUserIds = [], excludeUserIdsErr] = $(params.excludeUserIds).optional.array('id').$; + const [excludeUserIds = [], excludeUserIdsErr] = $(params.excludeUserIds).optional.array($().type(ID)).$; if (excludeUserIdsErr) return rej('invalid excludeUserIds param'); // Get 'includeUserUsernames' parameter - const [includeUserUsernames = [], includeUserUsernamesErr] = $(params.includeUserUsernames).optional.array('string').$; + const [includeUserUsernames = [], includeUserUsernamesErr] = $(params.includeUserUsernames).optional.array($().string()).$; if (includeUserUsernamesErr) return rej('invalid includeUserUsernames param'); // Get 'excludeUserUsernames' parameter - const [excludeUserUsernames = [], excludeUserUsernamesErr] = $(params.excludeUserUsernames).optional.array('string').$; + const [excludeUserUsernames = [], excludeUserUsernamesErr] = $(params.excludeUserUsernames).optional.array($().string()).$; if (excludeUserUsernamesErr) return rej('invalid excludeUserUsernames param'); // Get 'following' parameter diff --git a/src/server/api/endpoints/notes/show.ts b/src/server/api/endpoints/notes/show.ts index 67cdc3038bad5a06644fb2ad238530c44854ee94..266e0687e97d29ee4ba50f98cc33b9b87e2c91c5 100644 --- a/src/server/api/endpoints/notes/show.ts +++ b/src/server/api/endpoints/notes/show.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Note, { pack } from '../../../../models/note'; /** @@ -13,7 +13,7 @@ import Note, { pack } from '../../../../models/note'; */ module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'noteId' parameter - const [noteId, noteIdErr] = $(params.noteId).id().$; + const [noteId, noteIdErr] = $(params.noteId).type(ID).$; if (noteIdErr) return rej('invalid noteId param'); // Get note diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts index de30afea57d8beadf04207b8f71addd2f8fccb19..476d64158c42e71d6fcd3b3b0fbbca90f119bcb0 100644 --- a/src/server/api/endpoints/notes/timeline.ts +++ b/src/server/api/endpoints/notes/timeline.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Note from '../../../../models/note'; import Mute from '../../../../models/mute'; import ChannelWatching from '../../../../models/channel-watching'; @@ -17,11 +17,11 @@ module.exports = async (params, user, app) => { if (limitErr) throw 'invalid limit param'; // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$; if (sinceIdErr) throw 'invalid sinceId param'; // Get 'untilId' parameter - const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$; if (untilIdErr) throw 'invalid untilId param'; // Get 'sinceDate' parameter diff --git a/src/server/api/endpoints/notes/user-list-timeline.ts b/src/server/api/endpoints/notes/user-list-timeline.ts new file mode 100644 index 0000000000000000000000000000000000000000..bb94fa0ab979a699ba4c1ba7496e6a234dc54fa6 --- /dev/null +++ b/src/server/api/endpoints/notes/user-list-timeline.ts @@ -0,0 +1,179 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; import ID from '../../../../cafy-id'; +import Note from '../../../../models/note'; +import Mute from '../../../../models/mute'; +import { pack } from '../../../../models/note'; +import UserList from '../../../../models/user-list'; + +/** + * Get timeline of a user list + */ +module.exports = async (params, user, app) => { + // Get 'limit' parameter + const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$; + if (limitErr) throw 'invalid limit param'; + + // Get 'sinceId' parameter + const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$; + if (sinceIdErr) throw 'invalid sinceId param'; + + // Get 'untilId' parameter + const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$; + if (untilIdErr) throw 'invalid untilId param'; + + // Get 'sinceDate' parameter + const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$; + if (sinceDateErr) throw 'invalid sinceDate param'; + + // Get 'untilDate' parameter + const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$; + 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) { + throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified'; + } + + // Get 'includeMyRenotes' parameter + const [includeMyRenotes = true, includeMyRenotesErr] = $(params.includeMyRenotes).optional.boolean().$; + if (includeMyRenotesErr) throw 'invalid includeMyRenotes param'; + + // Get 'includeRenotedMyNotes' parameter + const [includeRenotedMyNotes = true, includeRenotedMyNotesErr] = $(params.includeRenotedMyNotes).optional.boolean().$; + if (includeRenotedMyNotesErr) throw 'invalid includeRenotedMyNotes param'; + + // Get 'listId' parameter + const [listId, listIdErr] = $(params.listId).type(ID).$; + if (listIdErr) throw 'invalid listId param'; + + const [list, mutedUserIds] = await Promise.all([ + // リストをå–å¾— + // Fetch the list + UserList.findOne({ + _id: listId, + userId: user._id + }), + + // ミュートã—ã¦ã„るユーザーをå–å¾— + Mute.find({ + muterId: user._id + }).then(ms => ms.map(m => m.muteeId)) + ]); + + if (list.userIds.length == 0) { + return []; + } + + //#region Construct query + const sort = { + _id: -1 + }; + + const listQuery = list.userIds.map(u => ({ + userId: u, + + // リプライã¯å«ã‚ãªã„(ãŸã ã—投稿者自身ã®æŠ•ç¨¿ã¸ã®ãƒªãƒ—ライã€è‡ªåˆ†ã®æŠ•ç¨¿ã¸ã®ãƒªãƒ—ライã€è‡ªåˆ†ã®ãƒªãƒ—ライã¯å«ã‚ã‚‹) + $or: [{ + // リプライã§ãªã„ + replyId: null + }, { // ã¾ãŸã¯ + // リプライã ãŒè¿”ä¿¡å…ˆãŒæŠ•ç¨¿è€…自身ã®æŠ•ç¨¿ + $expr: { + $eq: ['$_reply.userId', '$userId'] + } + }, { // ã¾ãŸã¯ + // リプライã ãŒè¿”ä¿¡å…ˆãŒè‡ªåˆ†(フォãƒãƒ¯ãƒ¼)ã®æŠ•ç¨¿ + '_reply.userId': user._id + }, { // ã¾ãŸã¯ + // 自分(フォãƒãƒ¯ãƒ¼)ãŒé€ä¿¡ã—ãŸãƒªãƒ—ライ + userId: user._id + }] + })); + + const query = { + $and: [{ + // リストã«å…¥ã£ã¦ã„る人ã®ã‚¿ã‚¤ãƒ ラインã¸ã®æŠ•ç¨¿ + $or: listQuery, + + // mute + userId: { + $nin: mutedUserIds + }, + '_reply.userId': { + $nin: mutedUserIds + }, + '_renote.userId': { + $nin: mutedUserIds + }, + }] + } as any; + + // MongoDBã§ã¯ãƒˆãƒƒãƒ—レベルã§å¦å®šãŒã§ããªã„ãŸã‚ã€De Morganã®æ³•å‰‡ã‚’利用ã—ã¦ã‚¯ã‚¨ãƒªã—ã¾ã™ã€‚ + // ã¤ã¾ã‚Šã€ã€Œã€Žè‡ªåˆ†ã®æŠ•ç¨¿ã‹ã¤Renoteã€ã§ã¯ãªã„ã€ã‚’「『自分ã®æŠ•ç¨¿ã§ã¯ãªã„ã€ã¾ãŸã¯ã€ŽRenoteã§ã¯ãªã„ã€ã€ã¨è¡¨ç¾ã—ã¾ã™ã€‚ + // for details: https://en.wikipedia.org/wiki/De_Morgan%27s_laws + + if (includeMyRenotes === false) { + query.$and.push({ + $or: [{ + userId: { $ne: user._id } + }, { + renoteId: null + }, { + text: { $ne: null } + }, { + mediaIds: { $ne: [] } + }, { + poll: { $ne: null } + }] + }); + } + + if (includeRenotedMyNotes === false) { + query.$and.push({ + $or: [{ + '_renote.userId': { $ne: user._id } + }, { + renoteId: null + }, { + text: { $ne: null } + }, { + mediaIds: { $ne: [] } + }, { + poll: { $ne: null } + }] + }); + } + + if (sinceId) { + sort._id = 1; + query._id = { + $gt: sinceId + }; + } else if (untilId) { + query._id = { + $lt: untilId + }; + } else if (sinceDate) { + sort._id = 1; + query.createdAt = { + $gt: new Date(sinceDate) + }; + } else if (untilDate) { + query.createdAt = { + $lt: new Date(untilDate) + }; + } + //#endregion + + // Issue query + const timeline = await Note + .find(query, { + limit: limit, + sort: sort + }); + + // Serialize + return await Promise.all(timeline.map(note => pack(note, user))); +}; diff --git a/src/server/api/endpoints/notifications/get_unread_count.ts b/src/server/api/endpoints/notifications/get_unread_count.ts index 283ecd63b14c6fa46e9ca1ef85424590d0e68017..600a80d1942ee6919696e956aeacea994241d247 100644 --- a/src/server/api/endpoints/notifications/get_unread_count.ts +++ b/src/server/api/endpoints/notifications/get_unread_count.ts @@ -6,10 +6,6 @@ import Mute from '../../../../models/mute'; /** * Get count of unread notifications - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { const mute = await Mute.find({ diff --git a/src/server/api/endpoints/notifications/mark_as_read_all.ts b/src/server/api/endpoints/notifications/mark_as_read_all.ts index 01c9145837c5e3eeceff7fa537aab3ca7b0f6ae6..dce3cb466360e51530dd00389430173de3a6bf20 100644 --- a/src/server/api/endpoints/notifications/mark_as_read_all.ts +++ b/src/server/api/endpoints/notifications/mark_as_read_all.ts @@ -6,10 +6,6 @@ import event from '../../../../publishers/stream'; /** * Mark as read all notifications - * - * @param {any} params - * @param {any} user - * @return {Promise<any>} */ module.exports = (params, user) => new Promise(async (res, rej) => { // Update documents diff --git a/src/server/api/endpoints/othello/games.ts b/src/server/api/endpoints/othello/games.ts index d05c1c258593f61ac7dba0a0c87bb386539bb13d..3b23b606371587d5a56124d0c3b4a9c7fd73faaa 100644 --- a/src/server/api/endpoints/othello/games.ts +++ b/src/server/api/endpoints/othello/games.ts @@ -1,4 +1,4 @@ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import OthelloGame, { pack } from '../../../../models/othello-game'; module.exports = (params, user) => new Promise(async (res, rej) => { @@ -11,11 +11,11 @@ module.exports = (params, user) => new Promise(async (res, rej) => { if (limitErr) return rej('invalid limit param'); // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$; if (sinceIdErr) return rej('invalid sinceId param'); // Get 'untilId' parameter - const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$; if (untilIdErr) return rej('invalid untilId param'); // Check if both of sinceId and untilId is specified diff --git a/src/server/api/endpoints/othello/games/show.ts b/src/server/api/endpoints/othello/games/show.ts index dd886936d43236f2682292fb05da7fa92503913d..d76c6556a2ac6f51f09749b61d72fd5cef7d302e 100644 --- a/src/server/api/endpoints/othello/games/show.ts +++ b/src/server/api/endpoints/othello/games/show.ts @@ -1,10 +1,10 @@ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../../cafy-id'; import OthelloGame, { pack } from '../../../../../models/othello-game'; import Othello from '../../../../../othello/core'; module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'gameId' parameter - const [gameId, gameIdErr] = $(params.gameId).id().$; + const [gameId, gameIdErr] = $(params.gameId).type(ID).$; if (gameIdErr) return rej('invalid gameId param'); const game = await OthelloGame.findOne({ _id: gameId }); diff --git a/src/server/api/endpoints/othello/match.ts b/src/server/api/endpoints/othello/match.ts index d9274f8f9c5419d9702c64f402730c1a86803000..b73b64437bb1b05f65a340cc55baae805b9750b7 100644 --- a/src/server/api/endpoints/othello/match.ts +++ b/src/server/api/endpoints/othello/match.ts @@ -1,4 +1,4 @@ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Matching, { pack as packMatching } from '../../../../models/othello-matching'; import OthelloGame, { pack as packGame } from '../../../../models/othello-game'; import User from '../../../../models/user'; @@ -7,7 +7,7 @@ import { eighteight } from '../../../../othello/maps'; module.exports = (params, user) => new Promise(async (res, rej) => { // Get 'userId' parameter - const [childId, childIdErr] = $(params.userId).id().$; + const [childId, childIdErr] = $(params.userId).type(ID).$; if (childIdErr) return rej('invalid userId param'); // Myself diff --git a/src/server/api/endpoints/users.ts b/src/server/api/endpoints/users.ts index ae33e8af0c5bd8c22778dc66c7839d44068cb0e0..5b389d452f713a6de490ef0c4f6f7c15085b61ea 100644 --- a/src/server/api/endpoints/users.ts +++ b/src/server/api/endpoints/users.ts @@ -6,10 +6,6 @@ import User, { pack } from '../../../models/user'; /** * Lists all users - * - * @param {any} params - * @param {any} me - * @return {Promise<any>} */ module.exports = (params, me) => new Promise(async (res, rej) => { // Get 'limit' parameter diff --git a/src/server/api/endpoints/users/followers.ts b/src/server/api/endpoints/users/followers.ts index 5f03326be856f1e1eaed9aa3933604038e9544f8..940b5ed9bce806162322dbbff0d466d2ecb4aa95 100644 --- a/src/server/api/endpoints/users/followers.ts +++ b/src/server/api/endpoints/users/followers.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import User from '../../../../models/user'; import Following from '../../../../models/following'; import { pack } from '../../../../models/user'; @@ -9,14 +9,10 @@ import { getFriendIds } from '../../common/get-friends'; /** * Get followers of a user - * - * @param {any} params - * @param {any} me - * @return {Promise<any>} */ module.exports = (params, me) => new Promise(async (res, rej) => { // Get 'userId' parameter - const [userId, userIdErr] = $(params.userId).id().$; + const [userId, userIdErr] = $(params.userId).type(ID).$; if (userIdErr) return rej('invalid userId param'); // Get 'iknow' parameter @@ -28,7 +24,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { if (limitErr) return rej('invalid limit param'); // Get 'cursor' parameter - const [cursor = null, cursorErr] = $(params.cursor).optional.id().$; + const [cursor = null, cursorErr] = $(params.cursor).optional.type(ID).$; if (cursorErr) return rej('invalid cursor param'); // Lookup user diff --git a/src/server/api/endpoints/users/following.ts b/src/server/api/endpoints/users/following.ts index 9fb135b24d57ae41ba6189020a9b5d952773b99d..63a73a2e27152379de0d2a80d7279e66c3042887 100644 --- a/src/server/api/endpoints/users/following.ts +++ b/src/server/api/endpoints/users/following.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import User from '../../../../models/user'; import Following from '../../../../models/following'; import { pack } from '../../../../models/user'; @@ -16,7 +16,7 @@ import { getFriendIds } from '../../common/get-friends'; */ module.exports = (params, me) => new Promise(async (res, rej) => { // Get 'userId' parameter - const [userId, userIdErr] = $(params.userId).id().$; + const [userId, userIdErr] = $(params.userId).type(ID).$; if (userIdErr) return rej('invalid userId param'); // Get 'iknow' parameter @@ -28,7 +28,7 @@ module.exports = (params, me) => new Promise(async (res, rej) => { if (limitErr) return rej('invalid limit param'); // Get 'cursor' parameter - const [cursor = null, cursorErr] = $(params.cursor).optional.id().$; + const [cursor = null, cursorErr] = $(params.cursor).optional.type(ID).$; if (cursorErr) return rej('invalid cursor param'); // Lookup user diff --git a/src/server/api/endpoints/users/get_frequently_replied_users.ts b/src/server/api/endpoints/users/get_frequently_replied_users.ts index 7a98f44e981ebc493345f0a91a4698bb4cf0c95b..4c00620a5215816d2003d960a85d3997ff7c3e20 100644 --- a/src/server/api/endpoints/users/get_frequently_replied_users.ts +++ b/src/server/api/endpoints/users/get_frequently_replied_users.ts @@ -1,13 +1,13 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import Note from '../../../../models/note'; import User, { pack } from '../../../../models/user'; module.exports = (params, me) => new Promise(async (res, rej) => { // Get 'userId' parameter - const [userId, userIdErr] = $(params.userId).id().$; + const [userId, userIdErr] = $(params.userId).type(ID).$; if (userIdErr) return rej('invalid userId param'); // Get 'limit' parameter diff --git a/src/server/api/endpoints/users/lists/create.ts b/src/server/api/endpoints/users/lists/create.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ae510f52b79dbecb36dba6e7da5652c7c67fa5e --- /dev/null +++ b/src/server/api/endpoints/users/lists/create.ts @@ -0,0 +1,25 @@ +/** + * Module dependencies + */ +import $ from 'cafy'; +import UserList, { pack } from '../../../../../models/user-list'; + +/** + * Create a user list + */ +module.exports = async (params, user) => new Promise(async (res, rej) => { + // Get 'title' parameter + const [title, titleErr] = $(params.title).string().range(1, 100).$; + if (titleErr) return rej('invalid title param'); + + // insert + const userList = await UserList.insert({ + createdAt: new Date(), + userId: user._id, + title: title, + userIds: [] + }); + + // Response + res(await pack(userList)); +}); diff --git a/src/server/api/endpoints/users/lists/list.ts b/src/server/api/endpoints/users/lists/list.ts new file mode 100644 index 0000000000000000000000000000000000000000..d19339a1f5bebaf9cf26d511905c635cb1db40aa --- /dev/null +++ b/src/server/api/endpoints/users/lists/list.ts @@ -0,0 +1,13 @@ +import UserList, { pack } from '../../../../../models/user-list'; + +/** + * Add a user to a user list + */ +module.exports = async (params, me) => new Promise(async (res, rej) => { + // Fetch lists + const userLists = await UserList.find({ + userId: me._id, + }); + + res(await Promise.all(userLists.map(x => pack(x)))); +}); diff --git a/src/server/api/endpoints/users/lists/push.ts b/src/server/api/endpoints/users/lists/push.ts new file mode 100644 index 0000000000000000000000000000000000000000..467c08efd491859805a0f7ed282caac3930010a8 --- /dev/null +++ b/src/server/api/endpoints/users/lists/push.ts @@ -0,0 +1,51 @@ +import $ from 'cafy'; import ID from '../../../../../cafy-id'; +import UserList from '../../../../../models/user-list'; +import User, { pack as packUser } from '../../../../../models/user'; +import { publishUserListStream } from '../../../../../publishers/stream'; + +/** + * Add a user to a user list + */ +module.exports = async (params, me) => new Promise(async (res, rej) => { + // Get 'listId' parameter + const [listId, listIdErr] = $(params.listId).type(ID).$; + if (listIdErr) return rej('invalid listId param'); + + // Fetch the list + const userList = await UserList.findOne({ + _id: listId, + userId: me._id, + }); + + if (userList == null) { + return rej('list not found'); + } + + // Get 'userId' parameter + const [userId, userIdErr] = $(params.userId).type(ID).$; + if (userIdErr) return rej('invalid userId param'); + + // Fetch the user + const user = await User.findOne({ + _id: userId + }); + + if (user == null) { + return rej('user not found'); + } + + if (userList.userIds.map(id => id.toHexString()).includes(user._id.toHexString())) { + return rej('the user already added'); + } + + // Push the user + await UserList.update({ _id: userList._id }, { + $push: { + userIds: user._id + } + }); + + res(); + + publishUserListStream(userList._id, 'userAdded', await packUser(user)); +}); diff --git a/src/server/api/endpoints/users/lists/show.ts b/src/server/api/endpoints/users/lists/show.ts new file mode 100644 index 0000000000000000000000000000000000000000..61e0f0463f2c926b0e22cdb7f4fc8e7217c79f73 --- /dev/null +++ b/src/server/api/endpoints/users/lists/show.ts @@ -0,0 +1,23 @@ +import $ from 'cafy'; import ID from '../../../../../cafy-id'; +import UserList, { pack } from '../../../../../models/user-list'; + +/** + * Show a user list + */ +module.exports = async (params, me) => new Promise(async (res, rej) => { + // Get 'listId' parameter + const [listId, listIdErr] = $(params.listId).type(ID).$; + if (listIdErr) return rej('invalid listId param'); + + // Fetch the list + const userList = await UserList.findOne({ + _id: listId, + userId: me._id, + }); + + if (userList == null) { + return rej('list not found'); + } + + res(await pack(userList)); +}); diff --git a/src/server/api/endpoints/users/notes.ts b/src/server/api/endpoints/users/notes.ts index bd4247c79c4a5709e9862f2e65a5af4adfc47cdb..dafa18bcc967d19c690549a11bc40d21d28a14d3 100644 --- a/src/server/api/endpoints/users/notes.ts +++ b/src/server/api/endpoints/users/notes.ts @@ -1,7 +1,7 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import getHostLower from '../../common/get-host-lower'; import Note, { pack } from '../../../../models/note'; import User from '../../../../models/user'; @@ -11,7 +11,7 @@ import User from '../../../../models/user'; */ module.exports = (params, me) => new Promise(async (res, rej) => { // Get 'userId' parameter - const [userId, userIdErr] = $(params.userId).optional.id().$; + const [userId, userIdErr] = $(params.userId).optional.type(ID).$; if (userIdErr) return rej('invalid userId param'); // Get 'username' parameter @@ -43,11 +43,11 @@ module.exports = (params, me) => new Promise(async (res, rej) => { if (limitErr) return rej('invalid limit param'); // Get 'sinceId' parameter - const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$; + const [sinceId, sinceIdErr] = $(params.sinceId).optional.type(ID).$; if (sinceIdErr) return rej('invalid sinceId param'); // Get 'untilId' parameter - const [untilId, untilIdErr] = $(params.untilId).optional.id().$; + const [untilId, untilIdErr] = $(params.untilId).optional.type(ID).$; if (untilIdErr) return rej('invalid untilId param'); // Get 'sinceDate' parameter diff --git a/src/server/api/endpoints/users/search_by_username.ts b/src/server/api/endpoints/users/search_by_username.ts index 5f6ececff94da31901f7a97b8dde51793b539592..91d9ad1f3a29f425ce15b76ce35d1044cc2509d4 100644 --- a/src/server/api/endpoints/users/search_by_username.ts +++ b/src/server/api/endpoints/users/search_by_username.ts @@ -6,10 +6,6 @@ import User, { pack } from '../../../../models/user'; /** * Search a user by username - * - * @param {any} params - * @param {any} me - * @return {Promise<any>} */ module.exports = (params, me) => new Promise(async (res, rej) => { // Get 'query' parameter diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts index 7e7f5dc4886cf142372914282fa15ea8fd78e588..141565ece6acc746a06b12a26ac64c3278ede204 100644 --- a/src/server/api/endpoints/users/show.ts +++ b/src/server/api/endpoints/users/show.ts @@ -1,22 +1,26 @@ /** * Module dependencies */ -import $ from 'cafy'; +import $ from 'cafy'; import ID from '../../../../cafy-id'; import User, { pack } from '../../../../models/user'; import resolveRemoteUser from '../../../../remote/resolve-user'; const cursorOption = { fields: { data: false } }; /** - * Show a user + * Show user(s) */ module.exports = (params, me) => new Promise(async (res, rej) => { let user; // Get 'userId' parameter - const [userId, userIdErr] = $(params.userId).optional.id().$; + const [userId, userIdErr] = $(params.userId).optional.type(ID).$; if (userIdErr) return rej('invalid userId param'); + // Get 'userIds' parameter + const [userIds, userIdsErr] = $(params.userIds).optional.array($().type(ID)).$; + if (userIdsErr) return rej('invalid userIds param'); + // Get 'username' parameter const [username, usernameErr] = $(params.username).optional.string().$; if (usernameErr) return rej('invalid username param'); @@ -25,32 +29,40 @@ module.exports = (params, me) => new Promise(async (res, rej) => { const [host, hostErr] = $(params.host).nullable.optional.string().$; if (hostErr) return rej('invalid host param'); - if (userId === undefined && typeof username !== 'string') { - return rej('userId or pair of username and host is required'); - } + if (userIds) { + const users = await User.find({ + _id: { + $in: userIds + } + }); - // Lookup user - if (typeof host === 'string') { - try { - user = await resolveRemoteUser(username, host, cursorOption); - } catch (e) { - console.warn(`failed to resolve remote user: ${e}`); - return rej('failed to resolve remote user'); - } + res(await Promise.all(users.map(u => pack(u, me, { + detail: true + })))); } else { - const q = userId !== undefined - ? { _id: userId } - : { usernameLower: username.toLowerCase(), host: null }; + // Lookup user + if (typeof host === 'string') { + try { + user = await resolveRemoteUser(username, host, cursorOption); + } catch (e) { + console.warn(`failed to resolve remote user: ${e}`); + return rej('failed to resolve remote user'); + } + } else { + const q = userId !== undefined + ? { _id: userId } + : { usernameLower: username.toLowerCase(), host: null }; - user = await User.findOne(q, cursorOption); + user = await User.findOne(q, cursorOption); - if (user === null) { - return rej('user not found'); + if (user === null) { + return rej('user not found'); + } } - } - // Send response - res(await pack(user, me, { - detail: true - })); + // Send response + res(await pack(user, me, { + detail: true + })); + } }); diff --git a/src/server/api/stream/user-list.ts b/src/server/api/stream/user-list.ts new file mode 100644 index 0000000000000000000000000000000000000000..ba03b97860afa2d4e5a646e9e2136336bca1354a --- /dev/null +++ b/src/server/api/stream/user-list.ts @@ -0,0 +1,14 @@ +import * as websocket from 'websocket'; +import * as redis from 'redis'; +import { ParsedUrlQuery } from 'querystring'; + +export default function(request: websocket.request, connection: websocket.connection, subscriber: redis.RedisClient, user: any): void { + const q = request.resourceURL.query as ParsedUrlQuery; + const listId = q.listId as string; + + // Subscribe stream + subscriber.subscribe(`misskey:user-list-stream:${listId}`); + subscriber.on('message', (_, data) => { + connection.send(data); + }); +} diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts index ce1325364962a0d895cf40dcfd65445b6f26d966..e4884ed7c42a621f752d8d5a043831f7b50d12a2 100644 --- a/src/server/api/streaming.ts +++ b/src/server/api/streaming.ts @@ -6,6 +6,7 @@ import config from '../../config'; import homeStream from './stream/home'; import localTimelineStream from './stream/local-timeline'; import globalTimelineStream from './stream/global-timeline'; +import userListStream from './stream/user-list'; import driveStream from './stream/drive'; import messagingStream from './stream/messaging'; import messagingIndexStream from './stream/messaging-index'; @@ -70,6 +71,7 @@ module.exports = (server: http.Server) => { request.resourceURL.pathname === '/' ? homeStream : request.resourceURL.pathname === '/local-timeline' ? localTimelineStream : request.resourceURL.pathname === '/global-timeline' ? globalTimelineStream : + request.resourceURL.pathname === '/user-list' ? userListStream : request.resourceURL.pathname === '/drive' ? driveStream : request.resourceURL.pathname === '/messaging' ? messagingStream : request.resourceURL.pathname === '/messaging-index' ? messagingIndexStream : diff --git a/src/server/index.ts b/src/server/index.ts index 2b5a9105075b667f60563c07f401413ad7fddfab..594f40c22f731fa7230b452a70f9cf30effceb15 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -11,6 +11,7 @@ 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 slow = require('koa-slow'); import activityPub from './activitypub'; import webFinger from './webfinger'; @@ -23,6 +24,11 @@ app.proxy = true; if (process.env.NODE_ENV != 'production') { // Logger app.use(logger()); + + // Delay + app.use(slow({ + delay: 1000 + })); } // Compress response diff --git a/src/services/note/create.ts b/src/services/note/create.ts index e5ad96898f7facbc97b68948f7217af217bef364..4808edfda4da19635d542ccc73d2668734c0958d 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -1,6 +1,6 @@ import Note, { pack, INote } from '../../models/note'; import User, { isLocalUser, IUser, isRemoteUser } from '../../models/user'; -import stream, { publishLocalTimelineStream, publishGlobalTimelineStream } from '../../publishers/stream'; +import stream, { publishLocalTimelineStream, publishGlobalTimelineStream, publishUserListStream } from '../../publishers/stream'; import Following from '../../models/following'; import { deliver } from '../../queue'; import renderNote from '../../remote/activitypub/renderer/note'; @@ -16,6 +16,7 @@ import pushSw from '../../publishers/push-sw'; import event from '../../publishers/stream'; import parse from '../../text/parse'; import { IApp } from '../../models/app'; +import UserList from '../../models/user-list'; export default async (user: IUser, data: { createdAt?: Date; @@ -110,60 +111,73 @@ export default async (user: IUser, data: { // タイムラインã¸ã®æŠ•ç¨¿ if (note.channelId == null) { - if (isLocalUser(user)) { - // Publish event to myself's stream - stream(note.userId, 'note', noteObj); - - // Publish note to local timeline stream - publishLocalTimelineStream(noteObj); - } - - // Publish note to global timeline stream - publishGlobalTimelineStream(noteObj); - - // Fetch all followers - const followers = await Following.find({ - followeeId: note.userId - }); - if (!silent) { - const render = async () => { - const content = data.renote && data.text == null - ? renderAnnounce(data.renote.uri ? data.renote.uri : await renderNote(data.renote)) - : renderCreate(await renderNote(note)); - return packAp(content); - }; - - // 投稿ãŒãƒªãƒ—ライã‹ã¤æŠ•ç¨¿è€…ãŒãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã¤ãƒªãƒ—ライ先ã®æŠ•ç¨¿ã®æŠ•ç¨¿è€…ãŒãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ãªã‚‰é…é€ - if (data.reply && isLocalUser(user) && isRemoteUser(data.reply._user)) { - deliver(user, await render(), data.reply._user.inbox); - } + if (isLocalUser(user)) { + // Publish event to myself's stream + stream(note.userId, 'note', noteObj); - // 投稿ãŒRenoteã‹ã¤æŠ•ç¨¿è€…ãŒãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã¤Renoteå…ƒã®æŠ•ç¨¿ã®æŠ•ç¨¿è€…ãŒãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ãªã‚‰é…é€ - if (data.renote && isLocalUser(user) && isRemoteUser(data.renote._user)) { - deliver(user, await render(), data.renote._user.inbox); + // Publish note to local timeline stream + publishLocalTimelineStream(noteObj); } - Promise.all(followers.map(async following => { - const follower = following._follower; - - if (isLocalUser(follower)) { - // ストーã‚ングã—ã¦ã„ãªã„å ´åˆ - if (!following.stalk) { - // ã“ã®æŠ•ç¨¿ãŒè¿”ä¿¡ãªã‚‰ã‚¹ã‚ップ - if (note.replyId && !note._reply.userId.equals(following.followerId) && !note._reply.userId.equals(note.userId)) return; + // Publish note to global timeline stream + publishGlobalTimelineStream(noteObj); + + // フォãƒãƒ¯ãƒ¼ã«é…ä¿¡ + Following.find({ + followeeId: note.userId + }).then(followers => { + followers.map(async following => { + const follower = following._follower; + + if (isLocalUser(follower)) { + // ストーã‚ングã—ã¦ã„ãªã„å ´åˆ + if (!following.stalk) { + // ã“ã®æŠ•ç¨¿ãŒè¿”ä¿¡ãªã‚‰ã‚¹ã‚ップ + if (note.replyId && !note._reply.userId.equals(following.followerId) && !note._reply.userId.equals(note.userId)) return; + } + + // Publish event to followers stream + stream(following.followerId, 'note', noteObj); + } else { + //#region APé…é€ + // フォãƒãƒ¯ãƒ¼ãŒãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã¤æŠ•ç¨¿è€…ãŒãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ãªã‚‰æŠ•ç¨¿ã‚’é…ä¿¡ + if (isLocalUser(user)) { + deliver(user, await render(), follower.inbox); + } + //#endergion } + }); + }); - // Publish event to followers stream - stream(following.followerId, 'note', noteObj); - } else { - // フォãƒãƒ¯ãƒ¼ãŒãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã¤æŠ•ç¨¿è€…ãŒãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ãªã‚‰æŠ•ç¨¿ã‚’é…ä¿¡ - if (isLocalUser(user)) { - deliver(user, await render(), follower.inbox); - } - } - })); + // リストã«é…ä¿¡ + UserList.find({ + userIds: note.userId + }).then(lists => { + lists.forEach(list => { + publishUserListStream(list._id, 'note', noteObj); + }); + }); + } + + //#region APé…é€ + const render = async () => { + const content = data.renote && data.text == null + ? renderAnnounce(data.renote.uri ? data.renote.uri : await renderNote(data.renote)) + : renderCreate(await renderNote(note)); + return packAp(content); + }; + + // 投稿ãŒãƒªãƒ—ライã‹ã¤æŠ•ç¨¿è€…ãŒãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã¤ãƒªãƒ—ライ先ã®æŠ•ç¨¿ã®æŠ•ç¨¿è€…ãŒãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ãªã‚‰é…é€ + if (data.reply && isLocalUser(user) && isRemoteUser(data.reply._user)) { + deliver(user, await render(), data.reply._user.inbox); + } + + // 投稿ãŒRenoteã‹ã¤æŠ•ç¨¿è€…ãŒãƒãƒ¼ã‚«ãƒ«ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã¤Renoteå…ƒã®æŠ•ç¨¿ã®æŠ•ç¨¿è€…ãŒãƒªãƒ¢ãƒ¼ãƒˆãƒ¦ãƒ¼ã‚¶ãƒ¼ãªã‚‰é…é€ + if (data.renote && isLocalUser(user) && isRemoteUser(data.renote._user)) { + deliver(user, await render(), data.renote._user.inbox); } + //#endergion } // ãƒãƒ£ãƒ³ãƒãƒ«ã¸ã®æŠ•ç¨¿ diff --git a/test/api.ts b/test/api.ts index 87bbb8ee16a204c169bd8b33d50e3948a16a136d..d8c163e920adda8d828998006475e7e61bf618df 100644 --- a/test/api.ts +++ b/test/api.ts @@ -15,11 +15,10 @@ process.on('unhandledRejection', console.dir); const fs = require('fs'); const _chai = require('chai'); const chaiHttp = require('chai-http'); -const should = _chai.should(); _chai.use(chaiHttp); -const server = require('../built/server/api'); +const server = require('../built/server/api').callback(); const db = require('../built/db/mongodb').default; const async = fn => (done) => { @@ -56,16 +55,6 @@ describe('API', () => { db.get('authSessions').drop() ])); - it('greet server', done => { - _chai.request(server) - .get('/') - .end((err, res) => { - res.should.have.status(200); - res.text.should.be.equal('YEE HAW'); - done(); - }); - }); - describe('signup', () => { it('ä¸æ£ãªãƒ¦ãƒ¼ã‚¶ãƒ¼åã§ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãŒä½œæˆã§ããªã„', async(async () => { const res = await request('/signup', { diff --git a/tslint.json b/tslint.json index d3f96000b9976561e9809e13677c6e2c0db9ff52..ae0df46b96e06eb06856427bc61f6eac13aef505 100644 --- a/tslint.json +++ b/tslint.json @@ -5,6 +5,7 @@ ], "jsRules": {}, "rules": { + "align": false, "indent": ["tab"], "quotemark": ["single"], "no-var-requires": false,