summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/app/admin/views/announcements.vue2
-rw-r--r--src/client/app/admin/views/emoji.vue2
-rw-r--r--src/client/app/admin/views/users.vue170
-rw-r--r--src/client/app/common/views/components/alert.vue2
-rw-r--r--src/client/app/common/views/components/ui/horizon-group.vue11
-rw-r--r--src/client/app/common/views/components/ui/input.vue40
-rw-r--r--src/client/app/common/views/components/ui/select.vue47
-rw-r--r--src/client/app/common/views/filters/index.ts7
-rw-r--r--src/client/app/common/views/filters/user.ts5
-rw-r--r--src/misc/acct/parse.ts1
-rw-r--r--src/models/user.ts4
-rw-r--r--src/remote/activitypub/models/note.ts2
-rw-r--r--src/remote/activitypub/models/person.ts4
-rw-r--r--src/server/api/endpoints/admin/reset-password.ts57
-rw-r--r--src/server/api/endpoints/admin/show-user.ts40
-rw-r--r--src/server/api/endpoints/users.ts45
-rw-r--r--src/server/api/endpoints/users/show.ts2
-rw-r--r--src/services/note/create.ts3
18 files changed, 383 insertions, 61 deletions
diff --git a/src/client/app/admin/views/announcements.vue b/src/client/app/admin/views/announcements.vue
index 31a2ab50b7..42e926af4d 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 6810340a3e..31c6b0ebfc 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 77ccf48e6a..b71ae38c23 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 27d876c87a..d48defe8a7 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 0d4eafae52..cd3d32beb1 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 76bb34da61..4d77810b47 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 da6f9696b5..a2cb600bc0 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 1759c19c2c..3dccbfc923 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 e5220229b7..9d4ae5c58b 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 0c00fccef6..164bd7bcd7 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 22eecb571b..bea0261dc2 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 c6bdd072a2..0f0f0bc2bf 100644
--- a/src/remote/activitypub/models/note.ts
+++ b/src/remote/activitypub/models/note.ts
@@ -111,7 +111,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 d78bc15c95..b2ca2eccae 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 0000000000..c072c12e0d
--- /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 0000000000..490b685352
--- /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 203b4a53c8..aef5bd8507 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 6e4cf514de..fd26554709 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 b512fe2dda..eac0185e7a 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
}