From 246cead2b1e179a02d81793a5515688539c788cd Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Fri, 23 Nov 2018 08:01:14 +0900 Subject: [PATCH] Improve user operations Resolve #2197 Resolve #3367 --- locales/ja-JP.yml | 23 ++- src/client/app/admin/views/announcements.vue | 2 +- src/client/app/admin/views/emoji.vue | 2 +- src/client/app/admin/views/users.vue | 170 ++++++++++++++++-- .../app/common/views/components/alert.vue | 2 +- .../views/components/ui/horizon-group.vue | 11 +- .../app/common/views/components/ui/input.vue | 40 +++-- .../app/common/views/components/ui/select.vue | 47 ++++- src/client/app/common/views/filters/index.ts | 7 + src/client/app/common/views/filters/user.ts | 5 +- src/misc/acct/parse.ts | 1 + src/models/user.ts | 4 +- src/remote/activitypub/models/note.ts | 2 +- src/remote/activitypub/models/person.ts | 4 +- .../api/endpoints/admin/reset-password.ts | 57 ++++++ src/server/api/endpoints/admin/show-user.ts | 40 +++++ src/server/api/endpoints/users.ts | 45 ++++- src/server/api/endpoints/users/show.ts | 2 +- src/services/note/create.ts | 3 + 19 files changed, 404 insertions(+), 63 deletions(-) create mode 100644 src/server/api/endpoints/admin/reset-password.ts create mode 100644 src/server/api/endpoints/admin/show-user.ts diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 8a27a1ad3..a799c67c8 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1151,16 +1151,35 @@ admin/views/charts.vue: network-usage: "通信量" admin/views/users.vue: - suspend-user: "ユーザーの凍結" + operation: "操作" + username-or-userid: "ユーザー名またはユーザーID" + user-not-found: "ユーザーが見つかりません" + lookup: "照会" + reset-password: "パスワードをリセット" + password-updated: "パスワードは現在「{password}」です" suspend: "凍結" suspended: "凍結しました" unsuspend: "凍結の解除" unsuspended: "凍結を解除しました" - verify-user: "ユーザーの公式アカウント設定" verify: "公式アカウントにする" verified: "公式アカウントにしました" unverify: "公式アカウントを解除する" unverified: "公式アカウントを解除しました" + users: + title: "ユーザー" + sort: + title: "ソート" + createdAtAsc: "登録日時が古い順" + createdAtDesc: "登録日時が新しい順" + updatedAtAsc: "最終更新日時が古い順" + updatedAtDesc: "最終更新日時が新しい順" + origin: + title: "オリジン" + combined: "ローカル+リモート" + local: "ローカル" + remote: "リモート" + createdAt: "登録日時" + updatedAt: "更新日時" admin/views/moderators.vue: add-moderator: diff --git a/src/client/app/admin/views/announcements.vue b/src/client/app/admin/views/announcements.vue index 31a2ab50b..42e926af4 100644 --- a/src/client/app/admin/views/announcements.vue +++ b/src/client/app/admin/views/announcements.vue @@ -9,7 +9,7 @@ <ui-textarea v-model="announcement.text"> <span>{{ $t('text') }}</span> </ui-textarea> - <ui-horizon-group> + <ui-horizon-group class="fit-bottom"> <ui-button @click="save()"><fa :icon="['far', 'save']"/> {{ $t('save') }}</ui-button> <ui-button @click="remove(i)"><fa :icon="['far', 'trash-alt']"/> {{ $t('remove') }}</ui-button> </ui-horizon-group> diff --git a/src/client/app/admin/views/emoji.vue b/src/client/app/admin/views/emoji.vue index 6810340a3..31c6b0ebf 100644 --- a/src/client/app/admin/views/emoji.vue +++ b/src/client/app/admin/views/emoji.vue @@ -38,7 +38,7 @@ <i slot="icon"><fa icon="link"/></i> <span>{{ $t('add-emoji.url') }}</span> </ui-input> - <ui-horizon-group> + <ui-horizon-group class="fit-bottom"> <ui-button @click="updateEmoji(emoji)"><fa :icon="['far', 'save']"/> {{ $t('emojis.update') }}</ui-button> <ui-button @click="removeEmoji(emoji)"><fa :icon="['far', 'trash-alt']"/> {{ $t('emojis.remove') }}</ui-button> </ui-horizon-group> diff --git a/src/client/app/admin/views/users.vue b/src/client/app/admin/views/users.vue index 77ccf48e6..b71ae38c2 100644 --- a/src/client/app/admin/views/users.vue +++ b/src/client/app/admin/views/users.vue @@ -1,28 +1,63 @@ <template> <div class="ucnffhbtogqgscfmqcymwmmupoknpfsw"> <ui-card> - <div slot="title"><fa :icon="faCertificate"/> {{ $t('verify-user') }}</div> + <div slot="title"><fa :icon="faTerminal"/> {{ $t('operation') }}</div> <section class="fit-top"> - <ui-input v-model="verifyUsername" type="text"> - <span slot="prefix">@</span> + <ui-input v-model="target" type="text"> + <span>{{ $t('username-or-userid') }}</span> </ui-input> + <ui-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('reset-password') }}</ui-button> <ui-horizon-group> - <ui-button @click="verifyUser" :disabled="verifying">{{ $t('verify') }}</ui-button> + <ui-button @click="verifyUser" :disabled="verifying"><fa :icon="faCertificate"/> {{ $t('verify') }}</ui-button> <ui-button @click="unverifyUser" :disabled="unverifying">{{ $t('unverify') }}</ui-button> </ui-horizon-group> + <ui-horizon-group> + <ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button> + <ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button> + </ui-horizon-group> + <ui-button @click="showUser"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button> + <ui-textarea v-if="user" :value="user | json5" readonly tall style="margin-top:16px;"></ui-textarea> </section> </ui-card> <ui-card> - <div slot="title"><fa :icon="faSnowflake"/> {{ $t('suspend-user') }}</div> + <div slot="title"><fa :icon="faUsers"/> {{ $t('users.title') }}</div> <section class="fit-top"> - <ui-input v-model="suspendUsername" type="text"> - <span slot="prefix">@</span> - </ui-input> - <ui-horizon-group> - <ui-button @click="suspendUser" :disabled="suspending">{{ $t('suspend') }}</ui-button> - <ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button> + <ui-horizon-group inputs> + <ui-select v-model="sort"> + <span slot="label">{{ $t('users.sort.title') }}</span> + <option value="-createdAt">{{ $t('users.sort.createdAtAsc') }}</option> + <option value="+createdAt">{{ $t('users.sort.createdAtDesc') }}</option> + <option value="-updatedAt">{{ $t('users.sort.updatedAtAsc') }}</option> + <option value="+updatedAt">{{ $t('users.sort.updatedAtDesc') }}</option> + </ui-select> + <ui-select v-model="origin"> + <span slot="label">{{ $t('users.origin.title') }}</span> + <option value="combined">{{ $t('users.origin.combined') }}</option> + <option value="local">{{ $t('users.origin.local') }}</option> + <option value="remote">{{ $t('users.origin.remote') }}</option> + </ui-select> </ui-horizon-group> + <div class="kofvwchc" v-for="user in users"> + <div> + <a :href="user | userPage(null, true)"> + <mk-avatar class="avatar" :user="user" :disable-link="true"/> + </a> + </div> + <div> + <header> + <b>{{ user | userName }}</b> + <span class="username">@{{ user | acct }}</span> + </header> + <div> + <span>{{ $t('users.createdAt') }}: <mk-time :time="user.createdAt" mode="detail"/></span> + </div> + <div> + <span>{{ $t('users.updatedAt') }}: <mk-time :time="user.updatedAt" mode="detail"/></span> + </div> + </div> + </div> + <ui-button v-if="existMore" @click="fetchUsers">{{ $t('@.load-more') }}</ui-button> </section> </ui-card> </div> @@ -32,7 +67,7 @@ import Vue from 'vue'; import i18n from '../../i18n'; import parseAcct from "../../../../misc/acct/parse"; -import { faCertificate } from '@fortawesome/free-solid-svg-icons'; +import { faCertificate, faUsers, faTerminal, faSearch, faKey } from '@fortawesome/free-solid-svg-icons'; import { faSnowflake } from '@fortawesome/free-regular-svg-icons'; export default Vue.extend({ @@ -40,22 +75,81 @@ export default Vue.extend({ data() { return { - verifyUsername: null, + user: null, + target: null, verifying: false, unverifying: false, - suspendUsername: null, suspending: false, unsuspending: false, - faCertificate, faSnowflake + sort: '+createdAt', + origin: 'combined', + limit: 10, + offset: 0, + users: [], + existMore: false, + faTerminal, faCertificate, faUsers, faSnowflake, faSearch, faKey }; }, + watch: { + sort() { + this.users = []; + this.offset = 0; + this.fetchUsers(); + }, + + origin() { + this.users = []; + this.offset = 0; + this.fetchUsers(); + } + }, + + mounted() { + this.fetchUsers(); + }, + methods: { + async fetchUser() { + try { + return await this.$root.api('users/show', this.target.startsWith('@') ? parseAcct(this.target) : { userId: this.target }); + } catch (e) { + if (e == 'user not found') { + this.$root.alert({ + type: 'error', + text: this.$t('user-not-found') + }); + } else { + this.$root.alert({ + type: 'error', + text: e.toString() + }); + } + } + }, + + async showUser() { + const user = await this.fetchUser(); + this.$root.api('admin/show-user', { userId: user.id }).then(info => { + this.user = info; + }); + }, + + async resetPassword() { + const user = await this.fetchUser(); + this.$root.api('admin/reset-password', { userId: user.id }).then(res => { + this.$root.alert({ + type: 'success', + text: this.$t('password-updated', { password: res.password }) + }); + }); + }, + async verifyUser() { this.verifying = true; const process = async () => { - const user = await this.$root.api('users/show', parseAcct(this.verifyUsername)); + const user = await this.fetchUser(); await this.$root.api('admin/verify-user', { userId: user.id }); this.$root.alert({ type: 'success', @@ -77,7 +171,7 @@ export default Vue.extend({ this.unverifying = true; const process = async () => { - const user = await this.$root.api('users/show', parseAcct(this.verifyUsername)); + const user = await this.fetchUser(); await this.$root.api('admin/unverify-user', { userId: user.id }); this.$root.alert({ type: 'success', @@ -99,7 +193,7 @@ export default Vue.extend({ this.suspending = true; const process = async () => { - const user = await this.$root.api('users/show', parseAcct(this.suspendUsername)); + const user = await this.fetchUser(); await this.$root.api('admin/suspend-user', { userId: user.id }); this.$root.alert({ type: 'success', @@ -121,7 +215,7 @@ export default Vue.extend({ this.unsuspending = true; const process = async () => { - const user = await this.$root.api('users/show', parseAcct(this.suspendUsername)); + const user = await this.fetchUser(); await this.$root.api('admin/unsuspend-user', { userId: user.id }); this.$root.alert({ type: 'success', @@ -137,6 +231,24 @@ export default Vue.extend({ }); this.unsuspending = false; + }, + + fetchUsers() { + this.$root.api('users', { + origin: this.origin, + sort: this.sort, + offset: this.offset, + limit: this.limit + 1 + }).then(users => { + if (users.length == this.limit + 1) { + users.pop(); + this.existMore = true; + } else { + this.existMore = false; + } + this.users = this.users.concat(users); + this.offset += this.limit; + }); } } }); @@ -147,4 +259,24 @@ export default Vue.extend({ @media (min-width 500px) padding 16px + .kofvwchc + display flex + padding 16px 0 + border-top solid 1px var(--faceDivider) + + > div:first-child + > a + > .avatar + width 64px + height 64px + + > div:last-child + flex 1 + padding-left 16px + + > header + > .username + margin-left 8px + opacity 0.7 + </style> diff --git a/src/client/app/common/views/components/alert.vue b/src/client/app/common/views/components/alert.vue index 27d876c87..d48defe8a 100644 --- a/src/client/app/common/views/components/alert.vue +++ b/src/client/app/common/views/components/alert.vue @@ -5,7 +5,7 @@ <div class="icon" :class="type"><fa :icon="icon"/></div> <header v-if="title" v-html="title"></header> <div class="body" v-if="text" v-html="text"></div> - <ui-horizon-group no-grow class="buttons" v-if="!splash"> + <ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash"> <ui-button @click="ok" primary autofocus>OK</ui-button> <ui-button @click="cancel" v-if="showCancelButton">Cancel</ui-button> </ui-horizon-group> diff --git a/src/client/app/common/views/components/ui/horizon-group.vue b/src/client/app/common/views/components/ui/horizon-group.vue index 0d4eafae5..cd3d32beb 100644 --- a/src/client/app/common/views/components/ui/horizon-group.vue +++ b/src/client/app/common/views/components/ui/horizon-group.vue @@ -27,9 +27,17 @@ export default Vue.extend({ <style lang="stylus" scoped> .vnxwkwuf + margin 16px 0 + &.inputs margin 32px 0 + &.fit-top + margin-top 0 + + &.fit-bottom + margin-bottom 0 + &:not(.noGrow) display flex @@ -37,5 +45,6 @@ export default Vue.extend({ flex 1 > *:not(:last-child) - margin-right 16px + margin-right 16px !important + </style> diff --git a/src/client/app/common/views/components/ui/input.vue b/src/client/app/common/views/components/ui/input.vue index 76bb34da6..4d77810b4 100644 --- a/src/client/app/common/views/components/ui/input.vue +++ b/src/client/app/common/views/components/ui/input.vue @@ -9,27 +9,30 @@ <div class="prefix" ref="prefix"><slot name="prefix"></slot></div> <template v-if="type != 'file'"> <input ref="input" - :type="type" - v-model="v" - :disabled="disabled" - :required="required" - :readonly="readonly" - :pattern="pattern" - :autocomplete="autocomplete" - :spellcheck="spellcheck" - @focus="focused = true" - @blur="focused = false"> + :type="type" + v-model="v" + :disabled="disabled" + :required="required" + :readonly="readonly" + :pattern="pattern" + :autocomplete="autocomplete" + :spellcheck="spellcheck" + @focus="focused = true" + @blur="focused = false" + > </template> <template v-else> <input ref="input" - type="text" - :value="placeholder" - readonly - @click="chooseFile"> + type="text" + :value="placeholder" + readonly + @click="chooseFile" + > <input ref="file" - type="file" - :value="value" - @change="onChangeFile"> + type="file" + :value="value" + @change="onChangeFile" + > </template> <div class="suffix" ref="suffix"><slot name="suffix"></slot></div> </div> @@ -325,6 +328,9 @@ root(fill) margin 6px 0 font-size 13px + &:empty + display none + * margin 0 diff --git a/src/client/app/common/views/components/ui/select.vue b/src/client/app/common/views/components/ui/select.vue index da6f9696b..a2cb600bc 100644 --- a/src/client/app/common/views/components/ui/select.vue +++ b/src/client/app/common/views/components/ui/select.vue @@ -1,15 +1,17 @@ <template> -<div class="ui-select" :class="[{ focused, filled }, styl]"> +<div class="ui-select" :class="[{ focused, disabled, filled, inline }, styl]"> <div class="icon" ref="icon"><slot name="icon"></slot></div> <div class="input" @click="focus"> <span class="label" ref="label"><slot name="label"></slot></span> <div class="prefix" ref="prefix"><slot name="prefix"></slot></div> <select ref="input" - :value="v" - :required="required" - @input="$emit('input', $event.target.value)" - @focus="focused = true" - @blur="focused = false"> + :value="v" + :required="required" + :disabled="disabled" + @input="$emit('input', $event.target.value)" + @focus="focused = true" + @blur="focused = false" + > <slot></slot> </select> <div class="suffix"><slot name="suffix"></slot></div> @@ -22,6 +24,11 @@ import Vue from 'vue'; export default Vue.extend({ + inject: { + horizonGrouped: { + default: false + } + }, props: { value: { required: false @@ -30,11 +37,22 @@ export default Vue.extend({ type: Boolean, required: false }, + disabled: { + type: Boolean, + required: false + }, styl: { type: String, required: false, default: 'line' - } + }, + inline: { + type: Boolean, + required: false, + default(): boolean { + return this.horizonGrouped; + } + }, }, data() { return { @@ -122,7 +140,7 @@ root(fill) transition-duration 0.3s font-size 16px line-height 32px - color rgba(#000, 0.54) + color var(--inputLabel) pointer-events none //will-change transform transform-origin top left @@ -171,6 +189,9 @@ root(fill) margin 6px 0 font-size 13px + &:empty + display none + * margin 0 @@ -200,4 +221,14 @@ root(fill) &:not(.fill) root(false) + &.inline + display inline-block + margin 0 + + &.disabled + opacity 0.7 + + &, * + cursor not-allowed !important + </style> diff --git a/src/client/app/common/views/filters/index.ts b/src/client/app/common/views/filters/index.ts index 1759c19c2..3dccbfc92 100644 --- a/src/client/app/common/views/filters/index.ts +++ b/src/client/app/common/views/filters/index.ts @@ -1,3 +1,10 @@ +import Vue from 'vue'; +import * as JSON5 from 'json5'; + +Vue.filter('json5', x => { + return JSON5.stringify(x, null, 2); +}); + require('./bytes'); require('./number'); require('./user'); diff --git a/src/client/app/common/views/filters/user.ts b/src/client/app/common/views/filters/user.ts index e5220229b..9d4ae5c58 100644 --- a/src/client/app/common/views/filters/user.ts +++ b/src/client/app/common/views/filters/user.ts @@ -1,6 +1,7 @@ import Vue from 'vue'; import getAcct from '../../../../../misc/acct/render'; import getUserName from '../../../../../misc/get-user-name'; +import { url } from '../../../config'; Vue.filter('acct', user => { return getAcct(user); @@ -10,6 +11,6 @@ Vue.filter('userName', user => { return getUserName(user); }); -Vue.filter('userPage', (user, path?) => { - return `/@${Vue.filter('acct')(user)}${(path ? `/${path}` : '')}`; +Vue.filter('userPage', (user, path?, absolute = false) => { + return `${absolute ? url : ''}/@${Vue.filter('acct')(user)}${(path ? `/${path}` : '')}`; }); diff --git a/src/misc/acct/parse.ts b/src/misc/acct/parse.ts index 0c00fccef..164bd7bcd 100644 --- a/src/misc/acct/parse.ts +++ b/src/misc/acct/parse.ts @@ -1,4 +1,5 @@ export default (acct: string) => { + if (acct.startsWith('@')) acct = acct.substr(1); const splitted = acct.split('@', 2); return { username: splitted[0], host: splitted[1] || null }; }; diff --git a/src/models/user.ts b/src/models/user.ts index 22eecb571..bea0261dc 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -26,6 +26,7 @@ export default User; type IUserBase = { _id: mongo.ObjectID; createdAt: Date; + updatedAt?: Date; deletedAt?: Date; followersCount: number; followingCount: number; @@ -104,7 +105,6 @@ export interface ILocalUser extends IUserBase { birthday: string; // 'YYYY-MM-DD' tags: string[]; }; - lastUsedAt: Date; isCat: boolean; isAdmin?: boolean; isModerator?: boolean; @@ -132,7 +132,7 @@ export interface IRemoteUser extends IUserBase { id: string; publicKeyPem: string; }; - updatedAt: Date; + lastFetchedAt: Date; isAdmin: false; isModerator: false; } diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts index 48a02e79b..82d6d267c 100644 --- a/src/remote/activitypub/models/note.ts +++ b/src/remote/activitypub/models/note.ts @@ -104,7 +104,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false }); // ユーザーの情報が古かったらついでに更新しておく - if (actor.updatedAt == null || Date.now() - actor.updatedAt.getTime() > 1000 * 60 * 60 * 24) { + if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { updatePerson(note.attributedTo); } diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts index d78bc15c9..b2ca2ecca 100644 --- a/src/remote/activitypub/models/person.ts +++ b/src/remote/activitypub/models/person.ts @@ -143,7 +143,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU avatarId: null, bannerId: null, createdAt: Date.parse(person.published) || null, - updatedAt: new Date(), + lastFetchedAt: new Date(), description: htmlToMFM(person.summary), followersCount, followingCount, @@ -298,7 +298,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje // Update user await User.update({ _id: exist._id }, { $set: { - updatedAt: new Date(), + lastFetchedAt: new Date(), inbox: person.inbox, sharedInbox: person.sharedInbox, featured: person.featured, diff --git a/src/server/api/endpoints/admin/reset-password.ts b/src/server/api/endpoints/admin/reset-password.ts new file mode 100644 index 000000000..c072c12e0 --- /dev/null +++ b/src/server/api/endpoints/admin/reset-password.ts @@ -0,0 +1,57 @@ +import $ from 'cafy'; +import ID, { transform } from '../../../../misc/cafy-id'; +import define from '../../define'; +import User from '../../../../models/user'; +import * as bcrypt from 'bcryptjs'; +import rndstr from 'rndstr'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーのパスワードをリセットします。', + }, + + requireCredential: true, + requireModerator: true, + + params: { + userId: { + validator: $.type(ID), + transform: transform, + desc: { + 'ja-JP': '対象のユーザーID', + 'en-US': 'The user ID which you want to suspend' + } + }, + } +}; + +export default define(meta, (ps) => new Promise(async (res, rej) => { + const user = await User.findOne({ + _id: ps.userId + }); + + if (user == null) { + return rej('user not found'); + } + + if (user.isAdmin) { + return rej('cannot reset password of admin'); + } + + const passwd = rndstr('a-zA-Z0-9', 8); + + // Generate hash of password + const hash = bcrypt.hashSync(passwd); + + await User.findOneAndUpdate({ + _id: user._id + }, { + $set: { + password: hash + } + }); + + res({ + password: passwd + }); +})); diff --git a/src/server/api/endpoints/admin/show-user.ts b/src/server/api/endpoints/admin/show-user.ts new file mode 100644 index 000000000..490b68535 --- /dev/null +++ b/src/server/api/endpoints/admin/show-user.ts @@ -0,0 +1,40 @@ +import $ from 'cafy'; +import ID, { transform } from '../../../../misc/cafy-id'; +import define from '../../define'; +import User from '../../../../models/user'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーの情報を取得します。', + }, + + requireCredential: true, + requireModerator: true, + + params: { + userId: { + validator: $.type(ID), + transform: transform, + desc: { + 'ja-JP': '対象のユーザーID', + 'en-US': 'The user ID which you want to suspend' + } + }, + } +}; + +export default define(meta, (ps, me) => new Promise(async (res, rej) => { + const user = await User.findOne({ + _id: ps.userId + }); + + if (user == null) { + return rej('user not found'); + } + + if (me.isModerator && user.isAdmin) { + return rej('cannot show info of admin'); + } + + res(user); +})); diff --git a/src/server/api/endpoints/users.ts b/src/server/api/endpoints/users.ts index 203b4a53c..aef5bd850 100644 --- a/src/server/api/endpoints/users.ts +++ b/src/server/api/endpoints/users.ts @@ -17,7 +17,23 @@ export const meta = { }, sort: { - validator: $.str.optional.or('+follower|-follower'), + validator: $.str.optional.or([ + '+follower', + '-follower', + '+createdAt', + '-createdAt', + '+updatedAt', + '-updatedAt', + ]), + }, + + origin: { + validator: $.str.optional.or([ + 'combined', + 'local', + 'remote', + ]), + default: 'local' } } }; @@ -33,6 +49,22 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { _sort = { followersCount: 1 }; + } else if (ps.sort == '+createdAt') { + _sort = { + createdAt: -1 + }; + } else if (ps.sort == '+updatedAt') { + _sort = { + updatedAt: -1 + }; + } else if (ps.sort == '-createdAt') { + _sort = { + createdAt: 1 + }; + } else if (ps.sort == '-updatedAt') { + _sort = { + updatedAt: 1 + }; } } else { _sort = { @@ -40,14 +72,17 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { }; } + const q = + ps.origin == 'local' ? { host: null } : + ps.origin == 'remote' ? { host: { $ne: null } } : + {}; + const users = await User - .find({ - host: null - }, { + .find(q, { limit: ps.limit, sort: _sort, skip: ps.offset }); - res(await Promise.all(users.map(user => pack(user, me)))); + res(await Promise.all(users.map(user => pack(user, me, { detail: true })))); })); diff --git a/src/server/api/endpoints/users/show.ts b/src/server/api/endpoints/users/show.ts index 6e4cf514d..fd2655470 100644 --- a/src/server/api/endpoints/users/show.ts +++ b/src/server/api/endpoints/users/show.ts @@ -80,7 +80,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { })); if (isRemoteUser(user)) { - if (user.updatedAt == null || Date.now() - user.updatedAt.getTime() > 1000 * 60 * 60 * 24) { + if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { resolveRemoteUser(ps.username, ps.host, { }, true); } } diff --git a/src/services/note/create.ts b/src/services/note/create.ts index b512fe2dd..eac0185e7 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -633,6 +633,9 @@ function saveReply(reply: INote, note: INote) { function incNotesCountOfUser(user: IUser) { User.update({ _id: user._id }, { + $set: { + updatedAt: new Date() + }, $inc: { notesCount: 1 }