summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2019-07-19 03:38:05 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2019-07-19 03:38:05 +0900
commitcd5b24d4eb494a4e9279348639e30b28bcdaa9f9 (patch)
treeabc65a9511b5affbcfd2ff063c8eda2c6251c637 /src
parentMerge branch 'develop' (diff)
parent11.26.0 (diff)
downloadsharkey-cd5b24d4eb494a4e9279348639e30b28bcdaa9f9.tar.gz
sharkey-cd5b24d4eb494a4e9279348639e30b28bcdaa9f9.tar.bz2
sharkey-cd5b24d4eb494a4e9279348639e30b28bcdaa9f9.zip
Merge branch 'develop'
Diffstat (limited to 'src')
-rw-r--r--src/client/app/admin/views/moderators.vue50
-rw-r--r--src/client/app/admin/views/users.user.vue18
-rw-r--r--src/client/app/admin/views/users.vue33
-rw-r--r--src/client/app/common/scripts/collect-page-vars.ts6
-rw-r--r--src/client/app/common/scripts/get-face.ts3
-rw-r--r--src/client/app/common/views/components/mention.vue17
-rw-r--r--src/client/app/common/views/components/misskey-flavored-markdown.vue1
-rw-r--r--src/client/app/common/views/components/page/page.block.vue3
-rw-r--r--src/client/app/common/views/components/page/page.radio-button.vue37
-rw-r--r--src/client/app/common/views/components/poll-editor.vue14
-rw-r--r--src/client/app/common/views/components/reaction-picker.vue1
-rw-r--r--src/client/app/common/views/components/settings/app-type.vue19
-rw-r--r--src/client/app/common/views/components/settings/profile.vue69
-rw-r--r--src/client/app/common/views/components/settings/settings.vue39
-rw-r--r--src/client/app/common/views/components/signup.vue8
-rw-r--r--src/client/app/common/views/components/url-preview.vue1
-rw-r--r--src/client/app/common/views/components/url.vue1
-rw-r--r--src/client/app/common/views/pages/page-editor/els/page-editor.el.radio-button.vue53
-rw-r--r--src/client/app/common/views/pages/page-editor/page-editor.blocks.vue3
-rw-r--r--src/client/app/common/views/pages/page-editor/page-editor.vue1
-rw-r--r--src/client/app/desktop/api/update-avatar.ts15
-rw-r--r--src/client/app/desktop/api/update-banner.ts15
-rw-r--r--src/client/app/desktop/views/components/drive.vue6
-rw-r--r--src/client/app/mobile/views/components/drive.vue6
-rw-r--r--src/db/postgre.ts2
-rw-r--r--src/models/entities/moderation-log.ts32
-rw-r--r--src/models/index.ts2
-rw-r--r--src/models/repositories/abuse-user-report.ts1
-rw-r--r--src/models/repositories/moderation-logs.ts31
-rw-r--r--src/models/repositories/user.ts1
-rw-r--r--src/remote/activitypub/renderer/person.ts15
-rw-r--r--src/server/api/common/signin.ts18
-rw-r--r--src/server/api/endpoints/admin/emoji/add.ts7
-rw-r--r--src/server/api/endpoints/admin/emoji/remove.ts7
-rw-r--r--src/server/api/endpoints/admin/queue/clear.ts5
-rw-r--r--src/server/api/endpoints/admin/show-moderation-logs.ts35
-rw-r--r--src/server/api/endpoints/admin/show-users.ts18
-rw-r--r--src/server/api/endpoints/admin/silence-user.ts7
-rw-r--r--src/server/api/endpoints/admin/suspend-user.ts13
-rw-r--r--src/server/api/endpoints/admin/unsilence-user.ts7
-rw-r--r--src/server/api/endpoints/admin/unsuspend-user.ts10
-rw-r--r--src/server/api/endpoints/admin/update-meta.ts5
-rw-r--r--src/server/api/endpoints/admin/vacuum.ts5
-rw-r--r--src/server/api/endpoints/federation/instances.ts12
-rw-r--r--src/server/api/endpoints/hashtags/users.ts2
-rw-r--r--src/server/api/endpoints/i/delete-account.ts4
-rw-r--r--src/server/api/endpoints/i/update.ts15
-rw-r--r--src/server/api/private/signin.ts22
-rw-r--r--src/server/api/private/signup.ts19
-rw-r--r--src/server/proxy/proxy-media.ts2
-rw-r--r--src/server/web/index.ts10
-rw-r--r--src/server/web/views/base.pug1
-rw-r--r--src/server/web/views/user.pug5
-rw-r--r--src/services/insert-moderation-log.ts13
-rw-r--r--src/services/suspend-user.ts34
-rw-r--r--src/services/unsuspend-user.ts35
56 files changed, 739 insertions, 75 deletions
diff --git a/src/client/app/admin/views/moderators.vue b/src/client/app/admin/views/moderators.vue
index bf7d951fc7..8ceab02d97 100644
--- a/src/client/app/admin/views/moderators.vue
+++ b/src/client/app/admin/views/moderators.vue
@@ -12,6 +12,31 @@
</ui-horizon-group>
</section>
</ui-card>
+
+ <ui-card>
+ <template #title>{{ $t('logs.title') }}</template>
+ <section class="fit-top">
+ <sequential-entrance animation="entranceFromTop" delay="25">
+ <div v-for="log in logs" :key="log.id" class="">
+ <ui-horizon-group inputs>
+ <ui-input :value="log.user | acct" type="text" readonly>
+ <span>{{ $t('logs.moderator') }}</span>
+ </ui-input>
+ <ui-input :value="log.type" type="text" readonly>
+ <span>{{ $t('logs.type') }}</span>
+ </ui-input>
+ <ui-input :value="log.createdAt | date" type="text" readonly>
+ <span>{{ $t('logs.at') }}</span>
+ </ui-input>
+ </ui-horizon-group>
+ <ui-textarea :value="JSON.stringify(log.info, null, 4)" readonly>
+ <span>{{ $t('logs.info') }}</span>
+ </ui-textarea>
+ </div>
+ </sequential-entrance>
+ <ui-button v-if="existMoreLogs" @click="fetchLogs">{{ $t('@.load-more') }}</ui-button>
+ </section>
+ </ui-card>
</div>
</template>
@@ -26,10 +51,17 @@ export default Vue.extend({
data() {
return {
username: '',
- changing: false
+ changing: false,
+ logs: [],
+ untilLogId: null,
+ existMoreLogs: false
};
},
+ created() {
+ this.fetchLogs();
+ },
+
methods: {
async add() {
this.changing = true;
@@ -74,6 +106,22 @@ export default Vue.extend({
this.changing = false;
},
+
+ fetchLogs() {
+ this.$root.api('admin/show-moderation-logs', {
+ untilId: this.untilId,
+ limit: 10 + 1
+ }).then(logs => {
+ if (logs.length == 10 + 1) {
+ logs.pop();
+ this.existMoreLogs = true;
+ } else {
+ this.existMoreLogs = false;
+ }
+ this.logs = this.logs.concat(logs);
+ this.untilLogId = this.logs[this.logs.length - 1].id;
+ });
+ },
}
});
</script>
diff --git a/src/client/app/admin/views/users.user.vue b/src/client/app/admin/views/users.user.vue
index 929fc8f4b3..9c3db2d6c2 100644
--- a/src/client/app/admin/views/users.user.vue
+++ b/src/client/app/admin/views/users.user.vue
@@ -5,7 +5,7 @@
<mk-avatar class="avatar" :user="user" :disable-link="true"/>
</a>
</div>
- <div>
+ <div @click="click(user.id)">
<header>
<b><mk-user-name :user="user"/></b>
<span class="username">@{{ user | acct }}</span>
@@ -32,7 +32,7 @@ import { faSnowflake } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
i18n: i18n('admin/views/users.vue'),
- props: ['user'],
+ props: ['user', 'click'],
data() {
return {
faSnowflake, faMicrophoneSlash
@@ -44,7 +44,7 @@ export default Vue.extend({
<style lang="stylus" scoped>
.kofvwchc
display flex
- padding 16px 0
+ padding 16px
border-top solid 1px var(--faceDivider)
> div:first-child
@@ -55,6 +55,7 @@ export default Vue.extend({
> div:last-child
flex 1
+ cursor pointer
padding-left 16px
@media (max-width 500px)
@@ -80,4 +81,15 @@ export default Vue.extend({
> .is-suspended
margin 0 0 0 .5em
color #4dabf7
+
+ &:hover
+ color var(--primaryForeground)
+ background var(--primary)
+ text-decoration none
+ border-radius 3px
+
+ &:active
+ color var(--primaryForeground)
+ background var(--primaryDarken10)
+ border-radius 3px
</style>
diff --git a/src/client/app/admin/views/users.vue b/src/client/app/admin/views/users.vue
index fd9f0dd8b2..92b23749ff 100644
--- a/src/client/app/admin/views/users.vue
+++ b/src/client/app/admin/views/users.vue
@@ -8,7 +8,7 @@
</ui-input>
<ui-button @click="showUser"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button>
- <div class="user" v-if="user">
+ <div ref="user" class="user" v-if="user" :key="user.id">
<x-user :user="user"/>
<div class="actions">
<ui-button v-if="user.host != null" @click="updateRemoteUser"><fa :icon="faSync"/> {{ $t('update-remote-user') }}</ui-button>
@@ -54,8 +54,16 @@
<option value="remote">{{ $t('users.origin.remote') }}</option>
</ui-select>
</ui-horizon-group>
+ <ui-horizon-group searchboxes>
+ <ui-input v-model="searchUsername" type="text" spellcheck="false" @input="fetchUsers(true)">
+ <span>{{ $t('username') }}</span>
+ </ui-input>
+ <ui-input v-model="searchHost" type="text" spellcheck="false" @input="fetchUsers(true)" :disabled="origin === 'local'">
+ <span>{{ $t('host') }}</span>
+ </ui-input>
+ </ui-horizon-group>
<sequential-entrance animation="entranceFromTop" delay="25">
- <x-user v-for="user in users" :user='user' :key="user.id"/>
+ <x-user v-for="user in users" :key="user.id" :user='user' :click="showUserOnClick"/>
</sequential-entrance>
<ui-button v-if="existMore" @click="fetchUsers">{{ $t('@.load-more') }}</ui-button>
</section>
@@ -85,6 +93,8 @@ export default Vue.extend({
sort: '+createdAt',
state: 'all',
origin: 'local',
+ searchUsername: '',
+ searchHost: '',
limit: 10,
offset: 0,
users: [],
@@ -107,6 +117,7 @@ export default Vue.extend({
},
origin() {
+ if (this.origin === 'local') this.searchHost = '';
this.users = [];
this.offset = 0;
this.fetchUsers();
@@ -157,6 +168,15 @@ export default Vue.extend({
this.target = '';
},
+ async showUserOnClick(userId: string) {
+ this.$root.api('admin/show-user', { userId: userId }).then(info => {
+ this.user = info;
+ this.$nextTick(() => {
+ this.$refs.user.scrollIntoView();
+ });
+ });
+ },
+
/** 処理対象ユーザーの情報を更新する */
async refreshUser() {
this.$root.api('admin/show-user', { userId: this.user.id }).then(info => {
@@ -308,13 +328,16 @@ export default Vue.extend({
return !confirm.canceled;
},
- fetchUsers() {
+ fetchUsers(truncate?: boolean) {
+ if (truncate) this.offset = 0;
this.$root.api('admin/show-users', {
state: this.state,
origin: this.origin,
sort: this.sort,
offset: this.offset,
- limit: this.limit + 1
+ limit: this.limit + 1,
+ username: this.searchUsername,
+ hostname: this.searchHost
}).then(users => {
if (users.length == this.limit + 1) {
users.pop();
@@ -322,7 +345,7 @@ export default Vue.extend({
} else {
this.existMore = false;
}
- this.users = this.users.concat(users);
+ this.users = truncate ? users : this.users.concat(users);
this.offset += this.limit;
});
}
diff --git a/src/client/app/common/scripts/collect-page-vars.ts b/src/client/app/common/scripts/collect-page-vars.ts
index 4c40d5d88e..a4096fb2c2 100644
--- a/src/client/app/common/scripts/collect-page-vars.ts
+++ b/src/client/app/common/scripts/collect-page-vars.ts
@@ -32,6 +32,12 @@ export function collectPageVars(content) {
type: 'number',
value: 0
});
+ } else if (x.type === 'radioButton') {
+ pageVars.push({
+ name: x.name,
+ type: 'string',
+ value: x.default || ''
+ });
} else if (x.children) {
collect(x.children);
}
diff --git a/src/client/app/common/scripts/get-face.ts b/src/client/app/common/scripts/get-face.ts
index b523948bd3..19f2bdb064 100644
--- a/src/client/app/common/scripts/get-face.ts
+++ b/src/client/app/common/scripts/get-face.ts
@@ -4,7 +4,8 @@ const faces = [
'🐡( \'-\' 🐡 )フグパンチ!!!!',
'✌️(´・_・`)✌️',
'(。>﹏<。)',
- '(Δ・x・Δ)'
+ '(Δ・x・Δ)',
+ '(コ`・ヘ・´ケ)'
];
export default () => faces[Math.floor(Math.random() * faces.length)];
diff --git a/src/client/app/common/views/components/mention.vue b/src/client/app/common/views/components/mention.vue
index f212fd3ca5..4e9f9e90d6 100644
--- a/src/client/app/common/views/components/mention.vue
+++ b/src/client/app/common/views/components/mention.vue
@@ -1,11 +1,17 @@
<template>
-<router-link class="ldlomzub" :to="`/${ canonical }`" v-user-preview="canonical">
+<router-link class="ldlomzub" :to="url" v-user-preview="canonical" v-if="url.startsWith('/')">
<span class="me" v-if="isMe">{{ $t('@.you') }}</span>
<span class="main">
<span class="username">@{{ username }}</span>
<span class="host" :class="{ fade: $store.state.settings.contrastedAcct }" v-if="(host != localHost) || $store.state.settings.showFullAcct">@{{ toUnicode(host) }}</span>
</span>
</router-link>
+<a class="ldlomzub" :href="url" target="_blank" rel="noopener" v-else>
+ <span class="main">
+ <span class="username">@{{ username }}</span>
+ <span class="host" :class="{ fade: $store.state.settings.contrastedAcct }">@{{ toUnicode(host) }}</span>
+ </span>
+</a>
</template>
<script lang="ts">
@@ -32,6 +38,15 @@ export default Vue.extend({
};
},
computed: {
+ url(): string {
+ switch (this.host) {
+ case 'twitter.com':
+ case 'github.com':
+ return `https://${this.host}/${this.username}`;
+ default:
+ return `/${this.canonical}`;
+ }
+ },
canonical(): string {
return this.host === localHost ? `@${this.username}` : `@${this.username}@${toUnicode(this.host)}`;
},
diff --git a/src/client/app/common/views/components/misskey-flavored-markdown.vue b/src/client/app/common/views/components/misskey-flavored-markdown.vue
index 64496f9c84..963efd9ab8 100644
--- a/src/client/app/common/views/components/misskey-flavored-markdown.vue
+++ b/src/client/app/common/views/components/misskey-flavored-markdown.vue
@@ -30,6 +30,7 @@ export default Vue.extend({
border-radius 4px
>>> .quote
+ display block
margin 8px
padding 6px 0 6px 12px
color var(--mfmQuote)
diff --git a/src/client/app/common/views/components/page/page.block.vue b/src/client/app/common/views/components/page/page.block.vue
index 1c421fc2c0..56d1822013 100644
--- a/src/client/app/common/views/components/page/page.block.vue
+++ b/src/client/app/common/views/components/page/page.block.vue
@@ -16,10 +16,11 @@ import XIf from './page.if.vue';
import XTextarea from './page.textarea.vue';
import XPost from './page.post.vue';
import XCounter from './page.counter.vue';
+import XRadioButton from './page.radio-button.vue';
export default Vue.extend({
components: {
- XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter
+ XText, XSection, XImage, XButton, XNumberInput, XTextInput, XTextareaInput, XTextarea, XPost, XSwitch, XIf, XCounter, XRadioButton
},
props: {
diff --git a/src/client/app/common/views/components/page/page.radio-button.vue b/src/client/app/common/views/components/page/page.radio-button.vue
new file mode 100644
index 0000000000..27c11bebad
--- /dev/null
+++ b/src/client/app/common/views/components/page/page.radio-button.vue
@@ -0,0 +1,37 @@
+<template>
+<div>
+ <div>{{ script.interpolate(value.title) }}</div>
+ <ui-radio v-for="x in value.values" v-model="v" :value="x" :key="x">{{ x }}</ui-radio>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ props: {
+ value: {
+ required: true
+ },
+ script: {
+ required: true
+ }
+ },
+
+ data() {
+ return {
+ v: this.value.default,
+ };
+ },
+
+ watch: {
+ v() {
+ this.script.aiScript.updatePageVar(this.value.name, this.v);
+ this.script.eval();
+ }
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+</style>
diff --git a/src/client/app/common/views/components/poll-editor.vue b/src/client/app/common/views/components/poll-editor.vue
index f7a4d3af8c..49940134c7 100644
--- a/src/client/app/common/views/components/poll-editor.vue
+++ b/src/client/app/common/views/components/poll-editor.vue
@@ -26,13 +26,19 @@
<option value="after">{{ $t('after') }}</option>
</ui-select>
<section v-if="expiration === 'at'">
- <ui-input v-model="atDate" type="date">{{ $t('deadline-date') }}</ui-input>
- <ui-input v-model="atTime" type="time">{{ $t('deadline-time') }}</ui-input>
+ <ui-input v-model="atDate" type="date">
+ <template #title>{{ $t('deadline-date') }}</template>
+ </ui-input>
+ <ui-input v-model="atTime" type="time">
+ <template #title>{{ $t('deadline-time') }}</template>
+ </ui-input>
</section>
<section v-if="expiration === 'after'">
- <ui-input v-model="after" type="number">{{ $t('interval') }}</ui-input>
+ <ui-input v-model="after" type="number">
+ <template #title>{{ $t('interval') }}</template>
+ </ui-input>
<ui-select v-model="unit">
- <template #label>{{ $t('unit') }}</template>
+ <template #title>{{ $t('unit') }}</template>
<option value="second">{{ $t('second') }}</option>
<option value="minute">{{ $t('minute') }}</option>
<option value="hour">{{ $t('hour') }}</option>
diff --git a/src/client/app/common/views/components/reaction-picker.vue b/src/client/app/common/views/components/reaction-picker.vue
index 970d430069..ff534d37ce 100644
--- a/src/client/app/common/views/components/reaction-picker.vue
+++ b/src/client/app/common/views/components/reaction-picker.vue
@@ -276,6 +276,7 @@ export default Vue.extend({
font-size 14px
color var(--popupFg)
border-bottom solid var(--lineWidth) var(--faceDivider)
+ line-height 20px
> .buttons
padding 4px 4px 8px 4px
diff --git a/src/client/app/common/views/components/settings/app-type.vue b/src/client/app/common/views/components/settings/app-type.vue
index 90ff28803b..d163f1e746 100644
--- a/src/client/app/common/views/components/settings/app-type.vue
+++ b/src/client/app/common/views/components/settings/app-type.vue
@@ -29,8 +29,25 @@ export default Vue.extend({
computed: {
appTypeForce: {
get() { return this.$store.state.device.appTypeForce; },
- set(value) { this.$store.commit('device/set', { key: 'appTypeForce', value }); }
+ set(value) {
+ this.$store.commit('device/set', { key: 'appTypeForce', value });
+ this.reload();
+ }
},
},
+
+ methods: {
+ reload() {
+ this.$root.dialog({
+ type: 'warning',
+ text: this.$t('@.reload-to-apply-the-setting'),
+ showCancelButton: true
+ }).then(({ canceled }) => {
+ if (!canceled) {
+ location.reload();
+ }
+ });
+ },
+ }
});
</script>
diff --git a/src/client/app/common/views/components/settings/profile.vue b/src/client/app/common/views/components/settings/profile.vue
index a22fd6df98..edfc5a9edf 100644
--- a/src/client/app/common/views/components/settings/profile.vue
+++ b/src/client/app/common/views/components/settings/profile.vue
@@ -51,6 +51,26 @@
<template #desc v-if="bannerUploading">{{ $t('uploading') }}<mk-ellipsis/></template>
</ui-input>
+ <div class="fields">
+ <header>{{ $t('profile-metadata') }}</header>
+ <ui-horizon-group>
+ <ui-input v-model="fieldName0">{{ $t('metadata-label') }}</ui-input>
+ <ui-input v-model="fieldValue0">{{ $t('metadata-content') }}</ui-input>
+ </ui-horizon-group>
+ <ui-horizon-group>
+ <ui-input v-model="fieldName1">{{ $t('metadata-label') }}</ui-input>
+ <ui-input v-model="fieldValue1">{{ $t('metadata-content') }}</ui-input>
+ </ui-horizon-group>
+ <ui-horizon-group>
+ <ui-input v-model="fieldName2">{{ $t('metadata-label') }}</ui-input>
+ <ui-input v-model="fieldValue2">{{ $t('metadata-content') }}</ui-input>
+ </ui-horizon-group>
+ <ui-horizon-group>
+ <ui-input v-model="fieldName3">{{ $t('metadata-label') }}</ui-input>
+ <ui-input v-model="fieldValue3">{{ $t('metadata-content') }}</ui-input>
+ </ui-horizon-group>
+ </div>
+
<ui-button @click="save(true)"><fa :icon="faSave"/> {{ $t('save') }}</ui-button>
</ui-form>
</section>
@@ -189,6 +209,17 @@ export default Vue.extend({
this.isLocked = this.$store.state.i.isLocked;
this.carefulBot = this.$store.state.i.carefulBot;
this.autoAcceptFollowed = this.$store.state.i.autoAcceptFollowed;
+
+ if (this.$store.state.i.fields) {
+ this.fieldName0 = this.$store.state.i.fields[0].name;
+ this.fieldValue0 = this.$store.state.i.fields[0].value;
+ this.fieldName1 = this.$store.state.i.fields[1].name;
+ this.fieldValue1 = this.$store.state.i.fields[1].value;
+ this.fieldName2 = this.$store.state.i.fields[2].name;
+ this.fieldValue2 = this.$store.state.i.fields[2].value;
+ this.fieldName3 = this.$store.state.i.fields[3].name;
+ this.fieldValue3 = this.$store.state.i.fields[3].value;
+ }
},
methods: {
@@ -237,6 +268,13 @@ export default Vue.extend({
},
save(notify) {
+ const fields = [
+ { name: this.fieldName0, value: this.fieldValue0 },
+ { name: this.fieldName1, value: this.fieldValue1 },
+ { name: this.fieldName2, value: this.fieldValue2 },
+ { name: this.fieldName3, value: this.fieldValue3 },
+ ];
+
this.saving = true;
this.$root.api('i/update', {
@@ -247,6 +285,7 @@ export default Vue.extend({
birthday: this.birthday || null,
avatarId: this.avatarId || undefined,
bannerId: this.bannerId || undefined,
+ fields,
isCat: !!this.isCat,
isBot: !!this.isBot,
isLocked: !!this.isLocked,
@@ -265,6 +304,29 @@ export default Vue.extend({
text: this.$t('saved')
});
}
+ }).catch(err => {
+ this.saving = false;
+ switch(err.id) {
+ case 'f419f9f8-2f4d-46b1-9fb4-49d3a2fd7191':
+ this.$root.dialog({
+ type: 'error',
+ title: this.$t('unable-to-process'),
+ text: this.$t('avatar-not-an-image')
+ });
+ break;
+ case '75aedb19-2afd-4e6d-87fc-67941256fa60':
+ this.$root.dialog({
+ type: 'error',
+ title: this.$t('unable-to-process'),
+ text: this.$t('banner-not-an-image')
+ });
+ break;
+ default:
+ this.$root.dialog({
+ type: 'error',
+ text: this.$t('unable-to-process')
+ });
+ }
});
},
@@ -366,4 +428,11 @@ export default Vue.extend({
height 72px
margin auto
+.fields
+ > header
+ padding 8px 0px
+ font-weight bold
+ > div
+ padding-left 16px
+
</style>
diff --git a/src/client/app/common/views/components/settings/settings.vue b/src/client/app/common/views/components/settings/settings.vue
index 281524979e..401d9423ae 100644
--- a/src/client/app/common/views/components/settings/settings.vue
+++ b/src/client/app/common/views/components/settings/settings.vue
@@ -143,13 +143,17 @@
<ui-input v-model="webSearchEngine">{{ $t('@._settings.web-search-engine') }}
<template #desc>{{ $t('@._settings.web-search-engine-desc') }}</template>
</ui-input>
+ <ui-button @click="save('webSearchEngine', webSearchEngine)"><fa :icon="faSave"/> {{ $t('@._settings.save') }}</ui-button>
</section>
<section v-if="!$root.isMobile">
<header>{{ $t('@._settings.paste') }}</header>
<ui-input v-model="pastedFileName">{{ $t('@._settings.pasted-file-name') }}
- <template #desc>{{ $t('@._settings.pasted-file-name-desc') }}</template>
+ <template v-if="pastedFileName === this.$store.state.settings.pastedFileName" #desc>{{ $t('@._settings.pasted-file-name-desc') }}</template>
+ <template v-else #desc>{{ pastedFileNamePreview() }}</template>
</ui-input>
+ <ui-button @click="save('pastedFileName', pastedFileName)"><fa :icon="faSave"/> {{ $t('@._settings.save') }}</ui-button>
+
<ui-switch v-model="pasteDialog">{{ $t('@._settings.paste-dialog') }}
<template #desc>{{ $t('@._settings.paste-dialog-desc') }}</template>
</ui-switch>
@@ -289,6 +293,8 @@ import XNotification from './notification.vue';
import { url, version } from '../../../../config';
import checkForUpdate from '../../../scripts/check-for-update';
+import { formatTimeString } from '../../../../../../misc/format-time-string';
+import { faSave } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
i18n: i18n(),
@@ -319,8 +325,11 @@ export default Vue.extend({
return {
meta: null,
version,
+ webSearchEngine: this.$store.state.settings.webSearchEngine,
+ pastedFileName : this.$store.state.settings.pastedFileName,
latestVersion: undefined,
- checkingForUpdate: false
+ checkingForUpdate: false,
+ faSave
};
},
computed: {
@@ -419,16 +428,6 @@ export default Vue.extend({
set(value) { this.$store.dispatch('settings/set', { key: 'defaultNoteVisibility', value }); }
},
- webSearchEngine: {
- get() { return this.$store.state.settings.webSearchEngine; },
- set(value) { this.$store.dispatch('settings/set', { key: 'webSearchEngine', value }); }
- },
-
- pastedFileName: {
- get() { return this.$store.state.settings.pastedFileName; },
- set(value) { this.$store.dispatch('settings/set', { key: 'pastedFileName', value }); }
- },
-
pasteDialog: {
get() { return this.$store.state.settings.pasteDialog; },
set(value) { this.$store.dispatch('settings/set', { key: 'pasteDialog', value }); }
@@ -565,6 +564,17 @@ export default Vue.extend({
}
});
},
+ save(key, value) {
+ this.$store.dispatch('settings/set', {
+ key,
+ value
+ }).then(() => {
+ this.$root.dialog({
+ type: 'success',
+ text: this.$t('@._settings.saved')
+ })
+ });
+ },
customizeHome() {
location.href = '/?customize';
},
@@ -600,7 +610,10 @@ export default Vue.extend({
const sound = new Audio(`${url}/assets/message.mp3`);
sound.volume = this.$store.state.device.soundVolume;
sound.play();
- }
+ },
+ pastedFileNamePreview() {
+ return `${formatTimeString(new Date(), this.pastedFileName).replace(/{{number}}/g, `1`)}.png`
+ },
}
});
</script>
diff --git a/src/client/app/common/views/components/signup.vue b/src/client/app/common/views/components/signup.vue
index 421d09a4dd..893f6575fb 100644
--- a/src/client/app/common/views/components/signup.vue
+++ b/src/client/app/common/views/components/signup.vue
@@ -43,7 +43,7 @@
</i18n>
</ui-switch>
<div v-if="meta.enableRecaptcha" class="g-recaptcha" :data-sitekey="meta.recaptchaSiteKey" style="margin: 16px 0;"></div>
- <ui-button type="submit" :disabled="!(meta.ToSUrl ? ToSAgreement : true) || passwordRetypeState == 'not-match'">{{ $t('create') }}</ui-button>
+ <ui-button type="submit" :disabled=" submitting || !(meta.ToSUrl ? ToSAgreement : true) || passwordRetypeState == 'not-match'">{{ $t('create') }}</ui-button>
</template>
</form>
</template>
@@ -70,6 +70,7 @@ export default Vue.extend({
passwordStrength: '',
passwordRetypeState: null,
meta: {},
+ submitting: false,
ToSAgreement: false
}
},
@@ -145,6 +146,9 @@ export default Vue.extend({
},
onSubmit() {
+ if (this.submitting) return;
+ this.submitting = true;
+
this.$root.api('signup', {
username: this.username,
password: this.password,
@@ -159,6 +163,8 @@ export default Vue.extend({
location.href = '/';
});
}).catch(() => {
+ this.submitting = false;
+
this.$root.dialog({
type: 'error',
text: this.$t('some-error')
diff --git a/src/client/app/common/views/components/url-preview.vue b/src/client/app/common/views/components/url-preview.vue
index 476c671e77..80aae5999d 100644
--- a/src/client/app/common/views/components/url-preview.vue
+++ b/src/client/app/common/views/components/url-preview.vue
@@ -66,6 +66,7 @@ export default Vue.extend({
(this.url.substr(local.length) === '/') ||
this.url.substr(local.length).startsWith('/@') ||
this.url.substr(local.length).startsWith('/notes/') ||
+ this.url.substr(local.length).startsWith('/tags/') ||
this.url.substr(local.length).startsWith('/pages/');
return {
local,
diff --git a/src/client/app/common/views/components/url.vue b/src/client/app/common/views/components/url.vue
index b1ca3f285c..3a304ad6e7 100644
--- a/src/client/app/common/views/components/url.vue
+++ b/src/client/app/common/views/components/url.vue
@@ -28,6 +28,7 @@ export default Vue.extend({
(this.url.substr(local.length) === '/') ||
this.url.substr(local.length).startsWith('/@') ||
this.url.substr(local.length).startsWith('/notes/') ||
+ this.url.substr(local.length).startsWith('/tags/') ||
this.url.substr(local.length).startsWith('/pages/'));
return {
local,
diff --git a/src/client/app/common/views/pages/page-editor/els/page-editor.el.radio-button.vue b/src/client/app/common/views/pages/page-editor/els/page-editor.el.radio-button.vue
new file mode 100644
index 0000000000..3401c46f47
--- /dev/null
+++ b/src/client/app/common/views/pages/page-editor/els/page-editor.el.radio-button.vue
@@ -0,0 +1,53 @@
+<template>
+<x-container @remove="() => $emit('remove')" :draggable="true">
+ <template #header><fa :icon="faBolt"/> {{ $t('blocks.radioButton') }}</template>
+
+ <section style="padding: 0 16px 16px 16px;">
+ <ui-input v-model="value.name"><template #prefix><fa :icon="faMagic"/></template><span>{{ $t('blocks._radioButton.name') }}</span></ui-input>
+ <ui-input v-model="value.title"><span>{{ $t('blocks._radioButton.title') }}</span></ui-input>
+ <ui-textarea v-model="values"><span>{{ $t('blocks._radioButton.values') }}</span></ui-textarea>
+ <ui-input v-model="value.default"><span>{{ $t('blocks._radioButton.default') }}</span></ui-input>
+ </section>
+</x-container>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faBolt, faMagic } from '@fortawesome/free-solid-svg-icons';
+import i18n from '../../../../../i18n';
+import XContainer from '../page-editor.container.vue';
+
+export default Vue.extend({
+ i18n: i18n('pages'),
+
+ components: {
+ XContainer
+ },
+
+ props: {
+ value: {
+ required: true
+ },
+ },
+
+ data() {
+ return {
+ values: '',
+ faBolt, faMagic
+ };
+ },
+
+ watch: {
+ values() {
+ Vue.set(this.value, 'values', this.values.split('\n'));
+ }
+ },
+
+ created() {
+ if (this.value.name == null) Vue.set(this.value, 'name', '');
+ if (this.value.title == null) Vue.set(this.value, 'title', '');
+ if (this.value.values == null) Vue.set(this.value, 'values', []);
+ this.values = this.value.values.join('\n');
+ },
+});
+</script>
diff --git a/src/client/app/common/views/pages/page-editor/page-editor.blocks.vue b/src/client/app/common/views/pages/page-editor/page-editor.blocks.vue
index c5f3419e7b..4d7293231f 100644
--- a/src/client/app/common/views/pages/page-editor/page-editor.blocks.vue
+++ b/src/client/app/common/views/pages/page-editor/page-editor.blocks.vue
@@ -19,10 +19,11 @@ import XSwitch from './els/page-editor.el.switch.vue';
import XIf from './els/page-editor.el.if.vue';
import XPost from './els/page-editor.el.post.vue';
import XCounter from './els/page-editor.el.counter.vue';
+import XRadioButton from './els/page-editor.el.radio-button.vue';
export default Vue.extend({
components: {
- XDraggable, XSection, XText, XImage, XButton, XTextarea, XTextInput, XTextareaInput, XNumberInput, XSwitch, XIf, XPost, XCounter
+ XDraggable, XSection, XText, XImage, XButton, XTextarea, XTextInput, XTextareaInput, XNumberInput, XSwitch, XIf, XPost, XCounter, XRadioButton
},
props: {
diff --git a/src/client/app/common/views/pages/page-editor/page-editor.vue b/src/client/app/common/views/pages/page-editor/page-editor.vue
index ade7d86991..0162915c38 100644
--- a/src/client/app/common/views/pages/page-editor/page-editor.vue
+++ b/src/client/app/common/views/pages/page-editor/page-editor.vue
@@ -342,6 +342,7 @@ export default Vue.extend({
label: this.$t('input-blocks'),
items: [
{ value: 'button', text: this.$t('blocks.button') },
+ { value: 'radioButton', text: this.$t('blocks.radioButton') },
{ value: 'textInput', text: this.$t('blocks.textInput') },
{ value: 'textareaInput', text: this.$t('blocks.textareaInput') },
{ value: 'numberInput', text: this.$t('blocks.numberInput') },
diff --git a/src/client/app/desktop/api/update-avatar.ts b/src/client/app/desktop/api/update-avatar.ts
index a095491b69..6b88b51ef1 100644
--- a/src/client/app/desktop/api/update-avatar.ts
+++ b/src/client/app/desktop/api/update-avatar.ts
@@ -83,6 +83,21 @@ export default ($root: any) => {
});
return i;
+ }).catch(err => {
+ switch (err.id) {
+ case 'f419f9f8-2f4d-46b1-9fb4-49d3a2fd7191':
+ $root.dialog({
+ type: 'error',
+ title: locale['desktop']['unable-to-process'],
+ text: locale['desktop']['invalid-filetype']
+ });
+ break;
+ default:
+ $root.dialog({
+ type: 'error',
+ text: locale['desktop']['unable-to-process']
+ });
+ }
});
};
diff --git a/src/client/app/desktop/api/update-banner.ts b/src/client/app/desktop/api/update-banner.ts
index c23a325364..09632b1941 100644
--- a/src/client/app/desktop/api/update-banner.ts
+++ b/src/client/app/desktop/api/update-banner.ts
@@ -83,6 +83,21 @@ export default ($root: any) => {
});
return i;
+ }).catch(err => {
+ switch (err.id) {
+ case '75aedb19-2afd-4e6d-87fc-67941256fa60':
+ $root.dialog({
+ type: 'error',
+ title: locale['desktop']['unable-to-process'],
+ text: locale['desktop']['invalid-filetype']
+ });
+ break;
+ default:
+ $root.dialog({
+ type: 'error',
+ text: locale['desktop']['unable-to-process']
+ });
+ }
});
};
diff --git a/src/client/app/desktop/views/components/drive.vue b/src/client/app/desktop/views/components/drive.vue
index a78c0040c3..ff4ff18e6e 100644
--- a/src/client/app/desktop/views/components/drive.vue
+++ b/src/client/app/desktop/views/components/drive.vue
@@ -22,19 +22,19 @@
>
<div class="selection" ref="selection"></div>
<div class="contents" ref="contents">
- <div class="folders" ref="foldersContainer" v-if="folders.length > 0">
+ <div class="folders" ref="foldersContainer" v-if="folders.length > 0 || moreFolders">
<x-folder v-for="folder in folders" :key="folder.id" class="folder" :folder="folder"/>
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
<div class="padding" v-for="n in 16"></div>
<ui-button v-if="moreFolders">{{ $t('@.load-more') }}</ui-button>
</div>
- <div class="files" ref="filesContainer" v-if="files.length > 0">
+ <div class="files" ref="filesContainer" v-if="files.length > 0 || moreFiles">
<x-file v-for="file in files" :key="file.id" class="file" :file="file"/>
<!-- SEE: https://stackoverflow.com/questions/18744164/flex-box-align-last-row-to-grid -->
<div class="padding" v-for="n in 16"></div>
<ui-button v-if="moreFiles" @click="fetchMoreFiles">{{ $t('@.load-more') }}</ui-button>
</div>
- <div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching">
+ <div class="empty" v-if="files.length == 0 && !moreFiles && folders.length == 0 && !moreFolders && !fetching">
<p v-if="draghover">{{ $t('empty-draghover') }}</p>
<p v-if="!draghover && folder == null"><strong>{{ $t('empty-drive') }}</strong><br/>{{ $t('empty-drive-description') }}</p>
<p v-if="!draghover && folder != null">{{ $t('empty-folder') }}</p>
diff --git a/src/client/app/mobile/views/components/drive.vue b/src/client/app/mobile/views/components/drive.vue
index 2613cfff99..f24c8492e5 100644
--- a/src/client/app/mobile/views/components/drive.vue
+++ b/src/client/app/mobile/views/components/drive.vue
@@ -25,17 +25,17 @@
<template v-if="folder.filesCount > 0">{{ folder.filesCount }} {{ $t('file-count') }}</template>
</p>
</div>
- <div class="folders" v-if="folders.length > 0">
+ <div class="folders" v-if="folders.length > 0 || moreFolders">
<x-folder class="folder" v-for="folder in folders" :key="folder.id" :folder="folder"/>
<p v-if="moreFolders">{{ $t('@.load-more') }}</p>
</div>
- <div class="files" v-if="files.length > 0">
+ <div class="files" v-if="files.length > 0 || moreFiles">
<x-file class="file" v-for="file in files" :key="file.id" :file="file"/>
<button class="more" v-if="moreFiles" @click="fetchMoreFiles">
{{ fetchingMoreFiles ? this.$t('@.loading') : this.$t('@.load-more') }}
</button>
</div>
- <div class="empty" v-if="files.length == 0 && folders.length == 0 && !fetching">
+ <div class="empty" v-if="files.length == 0 && !moreFiles && folders.length == 0 && !moreFolders && !fetching">
<p v-if="folder == null">{{ $t('nothing-in-drive') }}</p>
<p v-if="folder != null">{{ $t('folder-is-empty') }}</p>
</div>
diff --git a/src/db/postgre.ts b/src/db/postgre.ts
index 638d5720b7..16cfbd2b2f 100644
--- a/src/db/postgre.ts
+++ b/src/db/postgre.ts
@@ -47,6 +47,7 @@ import { UserSecurityKey } from '../models/entities/user-security-key';
import { AttestationChallenge } from '../models/entities/attestation-challenge';
import { Page } from '../models/entities/page';
import { PageLike } from '../models/entities/page-like';
+import { ModerationLog } from '../models/entities/moderation-log';
const sqlLogger = dbLogger.createSubLogger('sql', 'white', false);
@@ -124,6 +125,7 @@ export const entities = [
RegistrationTicket,
MessagingMessage,
Signin,
+ ModerationLog,
ReversiGame,
ReversiMatching,
...charts as any
diff --git a/src/models/entities/moderation-log.ts b/src/models/entities/moderation-log.ts
new file mode 100644
index 0000000000..33d3d683ae
--- /dev/null
+++ b/src/models/entities/moderation-log.ts
@@ -0,0 +1,32 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { User } from './user';
+import { id } from '../id';
+
+@Entity()
+export class ModerationLog {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the ModerationLog.'
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column(id())
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE'
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Column('varchar', {
+ length: 128,
+ })
+ public type: string;
+
+ @Column('jsonb')
+ public info: Record<string, any>;
+}
diff --git a/src/models/index.ts b/src/models/index.ts
index 888fd53f36..388bdc8f6f 100644
--- a/src/models/index.ts
+++ b/src/models/index.ts
@@ -42,6 +42,7 @@ import { UserSecurityKey } from './entities/user-security-key';
import { HashtagRepository } from './repositories/hashtag';
import { PageRepository } from './repositories/page';
import { PageLikeRepository } from './repositories/page-like';
+import { ModerationLogRepository } from './repositories/moderation-logs';
export const Apps = getCustomRepository(AppRepository);
export const Notes = getCustomRepository(NoteRepository);
@@ -86,3 +87,4 @@ export const ReversiMatchings = getCustomRepository(ReversiMatchingRepository);
export const Logs = getRepository(Log);
export const Pages = getCustomRepository(PageRepository);
export const PageLikes = getCustomRepository(PageLikeRepository);
+export const ModerationLogs = getCustomRepository(ModerationLogRepository);
diff --git a/src/models/repositories/abuse-user-report.ts b/src/models/repositories/abuse-user-report.ts
index 61d0d6e229..bff64c770c 100644
--- a/src/models/repositories/abuse-user-report.ts
+++ b/src/models/repositories/abuse-user-report.ts
@@ -14,6 +14,7 @@ export class AbuseUserReportRepository extends Repository<AbuseUserReport> {
return await awaitAll({
id: report.id,
createdAt: report.createdAt,
+ comment: report.comment,
reporterId: report.reporterId,
userId: report.userId,
reporter: Users.pack(report.reporter || report.reporterId, null, {
diff --git a/src/models/repositories/moderation-logs.ts b/src/models/repositories/moderation-logs.ts
new file mode 100644
index 0000000000..d6e04795bb
--- /dev/null
+++ b/src/models/repositories/moderation-logs.ts
@@ -0,0 +1,31 @@
+import { EntityRepository, Repository } from 'typeorm';
+import { Users } from '..';
+import { ModerationLog } from '../entities/moderation-log';
+import { ensure } from '../../prelude/ensure';
+import { awaitAll } from '../../prelude/await-all';
+
+@EntityRepository(ModerationLog)
+export class ModerationLogRepository extends Repository<ModerationLog> {
+ public async pack(
+ src: ModerationLog['id'] | ModerationLog,
+ ) {
+ const log = typeof src === 'object' ? src : await this.findOne(src).then(ensure);
+
+ return await awaitAll({
+ id: log.id,
+ createdAt: log.createdAt,
+ type: log.type,
+ info: log.info,
+ userId: log.userId,
+ user: Users.pack(log.user || log.userId, null, {
+ detail: true
+ }),
+ });
+ }
+
+ public packMany(
+ reports: any[],
+ ) {
+ return Promise.all(reports.map(x => this.pack(x)));
+ }
+}
diff --git a/src/models/repositories/user.ts b/src/models/repositories/user.ts
index 4e85fd7b93..a04b87f77c 100644
--- a/src/models/repositories/user.ts
+++ b/src/models/repositories/user.ts
@@ -148,6 +148,7 @@ export class UserRepository extends Repository<User> {
description: profile!.description,
location: profile!.location,
birthday: profile!.birthday,
+ fields: profile!.fields,
followersCount: user.followersCount,
followingCount: user.followingCount,
notesCount: user.notesCount,
diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts
index efe52cdefb..d4c018fb78 100644
--- a/src/remote/activitypub/renderer/person.ts
+++ b/src/remote/activitypub/renderer/person.ts
@@ -21,13 +21,24 @@ export async function renderPerson(user: ILocalUser) {
]);
const attachment: {
- type: string,
+ type: 'PropertyValue',
name: string,
value: string,
- verified_at?: string,
identifier?: IIdentifier
}[] = [];
+ if (profile.fields) {
+ for (const field of profile.fields) {
+ attachment.push({
+ type: 'PropertyValue',
+ name: field.name,
+ value: (field.value != null && field.value.match(/^https?:/))
+ ? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
+ : field.value
+ });
+ }
+ }
+
if (profile.twitter) {
attachment.push({
type: 'PropertyValue',
diff --git a/src/server/api/common/signin.ts b/src/server/api/common/signin.ts
index 0f4ee4ca11..37229ad1bc 100644
--- a/src/server/api/common/signin.ts
+++ b/src/server/api/common/signin.ts
@@ -2,6 +2,9 @@ import * as Koa from 'koa';
import config from '../../../config';
import { ILocalUser } from '../../../models/entities/user';
+import { Signins } from '../../../models';
+import { genId } from '../../../misc/gen-id';
+import { publishMainStream } from '../../../services/stream';
export default function(ctx: Koa.BaseContext, user: ILocalUser, redirect = false) {
if (redirect) {
@@ -24,4 +27,19 @@ export default function(ctx: Koa.BaseContext, user: ILocalUser, redirect = false
ctx.body = { i: user.token };
ctx.status = 200;
}
+
+ (async () => {
+ // Append signin history
+ const record = await Signins.save({
+ id: genId(),
+ createdAt: new Date(),
+ userId: user.id,
+ ip: ctx.ip,
+ headers: ctx.headers,
+ success: true
+ });
+
+ // Publish signin event
+ publishMainStream(user.id, 'signin', await Signins.pack(record));
+ })();
}
diff --git a/src/server/api/endpoints/admin/emoji/add.ts b/src/server/api/endpoints/admin/emoji/add.ts
index 5ba00afde8..8c21b1c73e 100644
--- a/src/server/api/endpoints/admin/emoji/add.ts
+++ b/src/server/api/endpoints/admin/emoji/add.ts
@@ -4,6 +4,7 @@ import { detectUrlMine } from '../../../../../misc/detect-url-mine';
import { Emojis } from '../../../../../models';
import { genId } from '../../../../../misc/gen-id';
import { getConnection } from 'typeorm';
+import { insertModerationLog } from '../../../../../services/insert-moderation-log';
export const meta = {
desc: {
@@ -31,7 +32,7 @@ export const meta = {
}
};
-export default define(meta, async (ps) => {
+export default define(meta, async (ps, me) => {
const type = await detectUrlMine(ps.url);
const emoji = await Emojis.save({
@@ -46,6 +47,10 @@ export default define(meta, async (ps) => {
await getConnection().queryResultCache!.remove(['meta_emojis']);
+ insertModerationLog(me, 'addEmoji', {
+ emojiId: emoji.id
+ });
+
return {
id: emoji.id
};
diff --git a/src/server/api/endpoints/admin/emoji/remove.ts b/src/server/api/endpoints/admin/emoji/remove.ts
index 3ebf933bc6..92c5f5f8c6 100644
--- a/src/server/api/endpoints/admin/emoji/remove.ts
+++ b/src/server/api/endpoints/admin/emoji/remove.ts
@@ -3,6 +3,7 @@ import define from '../../../define';
import { ID } from '../../../../../misc/cafy-id';
import { Emojis } from '../../../../../models';
import { getConnection } from 'typeorm';
+import { insertModerationLog } from '../../../../../services/insert-moderation-log';
export const meta = {
desc: {
@@ -21,7 +22,7 @@ export const meta = {
}
};
-export default define(meta, async (ps) => {
+export default define(meta, async (ps, me) => {
const emoji = await Emojis.findOne(ps.id);
if (emoji == null) throw new Error('emoji not found');
@@ -29,4 +30,8 @@ export default define(meta, async (ps) => {
await Emojis.delete(emoji.id);
await getConnection().queryResultCache!.remove(['meta_emojis']);
+
+ insertModerationLog(me, 'removeEmoji', {
+ emoji: emoji
+ });
});
diff --git a/src/server/api/endpoints/admin/queue/clear.ts b/src/server/api/endpoints/admin/queue/clear.ts
index f0fd00f1ad..03c1ae8463 100644
--- a/src/server/api/endpoints/admin/queue/clear.ts
+++ b/src/server/api/endpoints/admin/queue/clear.ts
@@ -1,5 +1,6 @@
import define from '../../../define';
import { destroy } from '../../../../../queue';
+import { insertModerationLog } from '../../../../../services/insert-moderation-log';
export const meta = {
tags: ['admin'],
@@ -10,8 +11,8 @@ export const meta = {
params: {}
};
-export default define(meta, async (ps) => {
+export default define(meta, async (ps, me) => {
destroy();
- return;
+ insertModerationLog(me, 'clearQueue');
});
diff --git a/src/server/api/endpoints/admin/show-moderation-logs.ts b/src/server/api/endpoints/admin/show-moderation-logs.ts
new file mode 100644
index 0000000000..bc67b3e55b
--- /dev/null
+++ b/src/server/api/endpoints/admin/show-moderation-logs.ts
@@ -0,0 +1,35 @@
+import $ from 'cafy';
+import { ID } from '../../../../misc/cafy-id';
+import define from '../../define';
+import { ModerationLogs } from '../../../../models';
+import { makePaginationQuery } from '../../common/make-pagination-query';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+
+ params: {
+ limit: {
+ validator: $.optional.num.range(1, 100),
+ default: 10
+ },
+
+ sinceId: {
+ validator: $.optional.type(ID),
+ },
+
+ untilId: {
+ validator: $.optional.type(ID),
+ },
+ }
+};
+
+export default define(meta, async (ps) => {
+ const query = makePaginationQuery(ModerationLogs.createQueryBuilder('report'), ps.sinceId, ps.untilId);
+
+ const reports = await query.take(ps.limit!).getMany();
+
+ return await ModerationLogs.packMany(reports);
+});
diff --git a/src/server/api/endpoints/admin/show-users.ts b/src/server/api/endpoints/admin/show-users.ts
index 8733d87a38..89e0cf1e2a 100644
--- a/src/server/api/endpoints/admin/show-users.ts
+++ b/src/server/api/endpoints/admin/show-users.ts
@@ -49,6 +49,16 @@ export const meta = {
'remote',
]),
default: 'local'
+ },
+
+ username: {
+ validator: $.optional.str,
+ default: null
+ },
+
+ hostname: {
+ validator: $.optional.str,
+ default: null
}
}
};
@@ -70,6 +80,14 @@ export default define(meta, async (ps, me) => {
case 'remote': query.andWhere('user.host IS NOT NULL'); break;
}
+ if (ps.username) {
+ query.andWhere('user.usernameLower like :username', { username: ps.username.toLowerCase() + '%' });
+ }
+
+ if (ps.hostname) {
+ query.andWhere('user.host like :hostname', { hostname: '%' + ps.hostname.toLowerCase() + '%' });
+ }
+
switch (ps.sort) {
case '+follower': query.orderBy('user.followersCount', 'DESC'); break;
case '-follower': query.orderBy('user.followersCount', 'ASC'); break;
diff --git a/src/server/api/endpoints/admin/silence-user.ts b/src/server/api/endpoints/admin/silence-user.ts
index 83aa88012a..8cc84aa1cc 100644
--- a/src/server/api/endpoints/admin/silence-user.ts
+++ b/src/server/api/endpoints/admin/silence-user.ts
@@ -2,6 +2,7 @@ import $ from 'cafy';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { Users } from '../../../../models';
+import { insertModerationLog } from '../../../../services/insert-moderation-log';
export const meta = {
desc: {
@@ -25,7 +26,7 @@ export const meta = {
}
};
-export default define(meta, async (ps) => {
+export default define(meta, async (ps, me) => {
const user = await Users.findOne(ps.userId as string);
if (user == null) {
@@ -39,4 +40,8 @@ export default define(meta, async (ps) => {
await Users.update(user.id, {
isSilenced: true
});
+
+ insertModerationLog(me, 'silence', {
+ targetId: user.id,
+ });
});
diff --git a/src/server/api/endpoints/admin/suspend-user.ts b/src/server/api/endpoints/admin/suspend-user.ts
index fa4d378708..6ba0d91505 100644
--- a/src/server/api/endpoints/admin/suspend-user.ts
+++ b/src/server/api/endpoints/admin/suspend-user.ts
@@ -4,6 +4,8 @@ import define from '../../define';
import deleteFollowing from '../../../../services/following/delete';
import { Users, Followings } from '../../../../models';
import { User } from '../../../../models/entities/user';
+import { insertModerationLog } from '../../../../services/insert-moderation-log';
+import { doPostSuspend } from '../../../../services/suspend-user';
export const meta = {
desc: {
@@ -27,7 +29,7 @@ export const meta = {
}
};
-export default define(meta, async (ps) => {
+export default define(meta, async (ps, me) => {
const user = await Users.findOne(ps.userId as string);
if (user == null) {
@@ -46,7 +48,14 @@ export default define(meta, async (ps) => {
isSuspended: true
});
- unFollowAll(user);
+ insertModerationLog(me, 'suspend', {
+ targetId: user.id,
+ });
+
+ (async () => {
+ await doPostSuspend(user).catch(e => {});
+ await unFollowAll(user).catch(e => {});
+ })();
});
async function unFollowAll(follower: User) {
diff --git a/src/server/api/endpoints/admin/unsilence-user.ts b/src/server/api/endpoints/admin/unsilence-user.ts
index f9b173366b..607c9b699a 100644
--- a/src/server/api/endpoints/admin/unsilence-user.ts
+++ b/src/server/api/endpoints/admin/unsilence-user.ts
@@ -2,6 +2,7 @@ import $ from 'cafy';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { Users } from '../../../../models';
+import { insertModerationLog } from '../../../../services/insert-moderation-log';
export const meta = {
desc: {
@@ -25,7 +26,7 @@ export const meta = {
}
};
-export default define(meta, async (ps) => {
+export default define(meta, async (ps, me) => {
const user = await Users.findOne(ps.userId as string);
if (user == null) {
@@ -35,4 +36,8 @@ export default define(meta, async (ps) => {
await Users.update(user.id, {
isSilenced: false
});
+
+ insertModerationLog(me, 'unsilence', {
+ targetId: user.id,
+ });
});
diff --git a/src/server/api/endpoints/admin/unsuspend-user.ts b/src/server/api/endpoints/admin/unsuspend-user.ts
index 08dae034d3..237585e276 100644
--- a/src/server/api/endpoints/admin/unsuspend-user.ts
+++ b/src/server/api/endpoints/admin/unsuspend-user.ts
@@ -2,6 +2,8 @@ import $ from 'cafy';
import { ID } from '../../../../misc/cafy-id';
import define from '../../define';
import { Users } from '../../../../models';
+import { insertModerationLog } from '../../../../services/insert-moderation-log';
+import { doPostUnsuspend } from '../../../../services/unsuspend-user';
export const meta = {
desc: {
@@ -25,7 +27,7 @@ export const meta = {
}
};
-export default define(meta, async (ps) => {
+export default define(meta, async (ps, me) => {
const user = await Users.findOne(ps.userId as string);
if (user == null) {
@@ -35,4 +37,10 @@ export default define(meta, async (ps) => {
await Users.update(user.id, {
isSuspended: false
});
+
+ insertModerationLog(me, 'unsuspend', {
+ targetId: user.id,
+ });
+
+ doPostUnsuspend(user);
});
diff --git a/src/server/api/endpoints/admin/update-meta.ts b/src/server/api/endpoints/admin/update-meta.ts
index 8e98d203ff..834faa42b9 100644
--- a/src/server/api/endpoints/admin/update-meta.ts
+++ b/src/server/api/endpoints/admin/update-meta.ts
@@ -2,6 +2,7 @@ import $ from 'cafy';
import define from '../../define';
import { getConnection } from 'typeorm';
import { Meta } from '../../../../models/entities/meta';
+import { insertModerationLog } from '../../../../services/insert-moderation-log';
export const meta = {
desc: {
@@ -401,7 +402,7 @@ export const meta = {
}
};
-export default define(meta, async (ps) => {
+export default define(meta, async (ps, me) => {
const set = {} as Partial<Meta>;
if (ps.announcements) {
@@ -653,4 +654,6 @@ export default define(meta, async (ps) => {
await transactionalEntityManager.save(Meta, set);
}
});
+
+ insertModerationLog(me, 'updateMeta');
});
diff --git a/src/server/api/endpoints/admin/vacuum.ts b/src/server/api/endpoints/admin/vacuum.ts
index 6990706282..4921e228e5 100644
--- a/src/server/api/endpoints/admin/vacuum.ts
+++ b/src/server/api/endpoints/admin/vacuum.ts
@@ -1,6 +1,7 @@
import $ from 'cafy';
import define from '../../define';
import { getConnection } from 'typeorm';
+import { insertModerationLog } from '../../../../services/insert-moderation-log';
export const meta = {
tags: ['admin'],
@@ -18,7 +19,7 @@ export const meta = {
}
};
-export default define(meta, async (ps) => {
+export default define(meta, async (ps, me) => {
const params: string[] = [];
if (ps.full) {
@@ -30,4 +31,6 @@ export default define(meta, async (ps) => {
}
getConnection().query('VACUUM ' + params.join(' '));
+
+ insertModerationLog(me, 'vacuum', ps);
});
diff --git a/src/server/api/endpoints/federation/instances.ts b/src/server/api/endpoints/federation/instances.ts
index 3c4e0037d6..bc0eb9a1d7 100644
--- a/src/server/api/endpoints/federation/instances.ts
+++ b/src/server/api/endpoints/federation/instances.ts
@@ -43,12 +43,12 @@ export default define(meta, async (ps, me) => {
switch (ps.sort) {
case '+notes': query.orderBy('instance.notesCount', 'DESC'); break;
case '-notes': query.orderBy('instance.notesCount', 'ASC'); break;
- case '+usersCount': query.orderBy('instance.usersCount', 'DESC'); break;
- case '-usersCount': query.orderBy('instance.usersCount', 'ASC'); break;
- case '+followingCount': query.orderBy('instance.followingCount', 'DESC'); break;
- case '-followingCount': query.orderBy('instance.followingCount', 'ASC'); break;
- case '+followersCount': query.orderBy('instance.followersCount', 'DESC'); break;
- case '-followersCount': query.orderBy('instance.followersCount', 'ASC'); break;
+ case '+users': query.orderBy('instance.usersCount', 'DESC'); break;
+ case '-users': query.orderBy('instance.usersCount', 'ASC'); break;
+ case '+following': query.orderBy('instance.followingCount', 'DESC'); break;
+ case '-following': query.orderBy('instance.followingCount', 'ASC'); break;
+ case '+followers': query.orderBy('instance.followersCount', 'DESC'); break;
+ case '-followers': query.orderBy('instance.followersCount', 'ASC'); break;
case '+caughtAt': query.orderBy('instance.caughtAt', 'DESC'); break;
case '-caughtAt': query.orderBy('instance.caughtAt', 'ASC'); break;
case '+lastCommunicatedAt': query.orderBy('instance.lastCommunicatedAt', 'DESC'); break;
diff --git a/src/server/api/endpoints/hashtags/users.ts b/src/server/api/endpoints/hashtags/users.ts
index 59210f4604..28a78ff8e6 100644
--- a/src/server/api/endpoints/hashtags/users.ts
+++ b/src/server/api/endpoints/hashtags/users.ts
@@ -59,7 +59,7 @@ export const meta = {
export default define(meta, async (ps, me) => {
const query = Users.createQueryBuilder('user')
- .where(':tag = ANY(user.tags)', { tag: ps.tag });
+ .where(':tag = ANY(user.tags)', { tag: ps.tag.toLowerCase() });
const recent = new Date(Date.now() - (1000 * 60 * 60 * 24 * 5));
diff --git a/src/server/api/endpoints/i/delete-account.ts b/src/server/api/endpoints/i/delete-account.ts
index 8ec85c9f41..b4950cb1fb 100644
--- a/src/server/api/endpoints/i/delete-account.ts
+++ b/src/server/api/endpoints/i/delete-account.ts
@@ -3,6 +3,7 @@ import * as bcrypt from 'bcryptjs';
import define from '../../define';
import { Users, UserProfiles } from '../../../../models';
import { ensure } from '../../../../prelude/ensure';
+import { doPostSuspend } from '../../../../services/suspend-user';
export const meta = {
requireCredential: true,
@@ -26,5 +27,8 @@ export default define(meta, async (ps, user) => {
throw new Error('incorrect password');
}
+ // 物理削除する前にDelete activityを送信する
+ await doPostSuspend(user).catch(e => {});
+
await Users.delete(user.id);
});
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index a454cdb940..149081e50b 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -77,6 +77,13 @@ export const meta = {
}
},
+ fields: {
+ validator: $.optional.arr($.object()).range(1, 4),
+ desc: {
+ 'ja-JP': 'プロフィール補足情報'
+ }
+ },
+
isLocked: {
validator: $.optional.bool,
desc: {
@@ -226,6 +233,14 @@ export default define(meta, async (ps, user, app) => {
profileUpdates.pinnedPageId = null;
}
+ if (ps.fields) {
+ profileUpdates.fields = ps.fields
+ .filter(x => typeof x.name === 'string' && x.name !== '' && typeof x.value === 'string' && x.value !== '')
+ .map(x => {
+ return { name: x.name, value: x.value };
+ });
+ }
+
//#region emojis/tags
let emojis = [] as string[];
diff --git a/src/server/api/private/signin.ts b/src/server/api/private/signin.ts
index eb267aa604..de0e35f500 100644
--- a/src/server/api/private/signin.ts
+++ b/src/server/api/private/signin.ts
@@ -1,7 +1,6 @@
import * as Koa from 'koa';
import * as bcrypt from 'bcryptjs';
import * as speakeasy from 'speakeasy';
-import { publishMainStream } from '../../../services/stream';
import signin from '../common/signin';
import config from '../../../config';
import { Users, Signins, UserProfiles, UserSecurityKeys, AttestationChallenges } from '../../../models';
@@ -53,34 +52,30 @@ export default async (ctx: Koa.BaseContext) => {
// Compare password
const same = await bcrypt.compare(password, profile.password!);
- async function fail(status?: number, failure?: {error: string}) {
+ async function fail(status?: number, failure?: { error: string }) {
// Append signin history
- const record = await Signins.save({
+ await Signins.save({
id: genId(),
createdAt: new Date(),
userId: user.id,
ip: ctx.ip,
headers: ctx.headers,
- success: !!(status || failure)
+ success: false
});
- // Publish signin event
- publishMainStream(user.id, 'signin', await Signins.pack(record));
-
- if (status && failure) {
- ctx.throw(status, failure);
- }
+ ctx.throw(status || 500, failure || { error: 'someting happened' });
}
if (!profile.twoFactorEnabled) {
if (same) {
signin(ctx, user);
+ return;
} else {
await fail(403, {
error: 'incorrect password'
});
+ return;
}
- return;
}
if (token) {
@@ -169,6 +164,7 @@ export default async (ctx: Koa.BaseContext) => {
if (isValid) {
signin(ctx, user);
+ return;
} else {
await fail(403, {
error: 'invalid challenge data'
@@ -191,6 +187,7 @@ export default async (ctx: Koa.BaseContext) => {
await fail(403, {
error: 'no keys found'
});
+ return;
}
// 32 byte challenge
@@ -219,6 +216,5 @@ export default async (ctx: Koa.BaseContext) => {
ctx.status = 200;
return;
}
-
- await fail();
+ // never get here
};
diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts
index ca197a6611..026fe7485b 100644
--- a/src/server/api/private/signup.ts
+++ b/src/server/api/private/signup.ts
@@ -5,7 +5,7 @@ import generateUserToken from '../common/generate-native-user-token';
import config from '../../../config';
import { fetchMeta } from '../../../misc/fetch-meta';
import * as recaptcha from 'recaptcha-promise';
-import { Users, RegistrationTickets } from '../../../models';
+import { Users, Signins, RegistrationTickets } from '../../../models';
import { genId } from '../../../misc/gen-id';
import { usersChart } from '../../../services/chart';
import { User } from '../../../models/entities/user';
@@ -104,6 +104,13 @@ export default async (ctx: Koa.BaseContext) => {
// Start transaction
await getConnection().transaction(async transactionalEntityManager => {
+ const exist = await transactionalEntityManager.findOne(User, {
+ usernameLower: username.toLowerCase(),
+ host: null
+ });
+
+ if (exist) throw 'already registered';
+
account = await transactionalEntityManager.save(new User({
id: genId(),
createdAt: new Date(),
@@ -130,6 +137,16 @@ export default async (ctx: Koa.BaseContext) => {
usersChart.update(account, true);
+ // Append signin history
+ await Signins.save({
+ id: genId(),
+ createdAt: new Date(),
+ userId: account.id,
+ ip: ctx.ip,
+ headers: ctx.headers,
+ success: true
+ });
+
const res = await Users.pack(account, account, {
detail: true,
includeSecrets: true
diff --git a/src/server/proxy/proxy-media.ts b/src/server/proxy/proxy-media.ts
index eadfab54a3..415eef6c34 100644
--- a/src/server/proxy/proxy-media.ts
+++ b/src/server/proxy/proxy-media.ts
@@ -33,7 +33,7 @@ export async function proxyMedia(ctx: Koa.BaseContext) {
};
}
- ctx.set('Content-Type', type);
+ ctx.set('Content-Type', image.type);
ctx.set('Cache-Control', 'max-age=31536000, immutable');
ctx.body = image.data;
} catch (e) {
diff --git a/src/server/web/index.ts b/src/server/web/index.ts
index 8cf6a75208..6c41bbde46 100644
--- a/src/server/web/index.ts
+++ b/src/server/web/index.ts
@@ -156,11 +156,17 @@ router.get('/@:user', async (ctx, next) => {
if (user != null) {
const profile = await UserProfiles.findOne(user.id).then(ensure);
const meta = await fetchMeta();
+ const me = profile.fields
+ ? profile.fields
+ .filter(filed => filed.value != null && filed.value.match(/^https?:/))
+ .map(field => field.value)
+ : [];
+
await ctx.render('user', {
- user, profile,
+ user, profile, me,
instanceName: meta.name || 'Misskey'
});
- ctx.set('Cache-Control', 'public, max-age=180');
+ ctx.set('Cache-Control', 'public, max-age=30');
} else {
// リモートユーザーなので
await next();
diff --git a/src/server/web/views/base.pug b/src/server/web/views/base.pug
index 733a306d56..16bea853e7 100644
--- a/src/server/web/views/base.pug
+++ b/src/server/web/views/base.pug
@@ -44,3 +44,4 @@ html
<svg viewBox="0 0 50 50">
<path fill=#fb4e4e d="M25.251,6.461c-10.318,0-18.683,8.365-18.683,18.683h4.068c0-8.071,6.543-14.615,14.615-14.615V6.461z" />
</svg>
+ block content
diff --git a/src/server/web/views/user.pug b/src/server/web/views/user.pug
index 9b257afb7b..6ff86b09be 100644
--- a/src/server/web/views/user.pug
+++ b/src/server/web/views/user.pug
@@ -36,3 +36,8 @@ block meta
link(rel='alternate' href=user.uri type='application/activity+json')
if profile.url
link(rel='alternate' href=profile.url type='text/html')
+
+block content
+ div#me
+ each m in me
+ a(rel='me' href=`${m}`) #{m}
diff --git a/src/services/insert-moderation-log.ts b/src/services/insert-moderation-log.ts
new file mode 100644
index 0000000000..33dab97259
--- /dev/null
+++ b/src/services/insert-moderation-log.ts
@@ -0,0 +1,13 @@
+import { ILocalUser } from '../models/entities/user';
+import { ModerationLogs } from '../models';
+import { genId } from '../misc/gen-id';
+
+export async function insertModerationLog(moderator: ILocalUser, type: string, info?: Record<string, any>) {
+ await ModerationLogs.save({
+ id: genId(),
+ createdAt: new Date(),
+ userId: moderator.id,
+ type: type,
+ info: info || {}
+ });
+}
diff --git a/src/services/suspend-user.ts b/src/services/suspend-user.ts
new file mode 100644
index 0000000000..a85188acbe
--- /dev/null
+++ b/src/services/suspend-user.ts
@@ -0,0 +1,34 @@
+import renderDelete from '../remote/activitypub/renderer/delete';
+import { renderActivity } from '../remote/activitypub/renderer';
+import { deliver } from '../queue';
+import config from '../config';
+import { User } from '../models/entities/user';
+import { Users, Followings } from '../models';
+import { Not, IsNull } from 'typeorm';
+
+export async function doPostSuspend(user: User) {
+ if (Users.isLocalUser(user)) {
+ // 知り得る全SharedInboxにDelete配信
+ const content = renderActivity(renderDelete(`${config.url}/users/${user.id}`, user));
+
+ const queue: string[] = [];
+
+ const followings = await Followings.find({
+ where: [
+ { followerSharedInbox: Not(IsNull()) },
+ { followeeSharedInbox: Not(IsNull()) }
+ ],
+ select: ['followerSharedInbox', 'followeeSharedInbox']
+ });
+
+ const inboxes = followings.map(x => x.followerSharedInbox || x.followeeSharedInbox);
+
+ for (const inbox of inboxes) {
+ if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
+ }
+
+ for (const inbox of queue) {
+ deliver(user as any, content, inbox);
+ }
+ }
+}
diff --git a/src/services/unsuspend-user.ts b/src/services/unsuspend-user.ts
new file mode 100644
index 0000000000..6cab375821
--- /dev/null
+++ b/src/services/unsuspend-user.ts
@@ -0,0 +1,35 @@
+import renderDelete from '../remote/activitypub/renderer/delete';
+import renderUndo from '../remote/activitypub/renderer/undo';
+import { renderActivity } from '../remote/activitypub/renderer';
+import { deliver } from '../queue';
+import config from '../config';
+import { User } from '../models/entities/user';
+import { Users, Followings } from '../models';
+import { Not, IsNull } from 'typeorm';
+
+export async function doPostUnsuspend(user: User) {
+ if (Users.isLocalUser(user)) {
+ // 知り得る全SharedInboxにUndo Delete配信
+ const content = renderActivity(renderUndo(renderDelete(`${config.url}/users/${user.id}`, user), user));
+
+ const queue: string[] = [];
+
+ const followings = await Followings.find({
+ where: [
+ { followerSharedInbox: Not(IsNull()) },
+ { followeeSharedInbox: Not(IsNull()) }
+ ],
+ select: ['followerSharedInbox', 'followeeSharedInbox']
+ });
+
+ const inboxes = followings.map(x => x.followerSharedInbox || x.followeeSharedInbox);
+
+ for (const inbox of inboxes) {
+ if (inbox != null && !queue.includes(inbox)) queue.push(inbox);
+ }
+
+ for (const inbox of queue) {
+ deliver(user as any, content, inbox);
+ }
+ }
+}