mirror of
https://github.com/hotomoe/hotomoe
synced 2024-11-25 15:46:17 +09:00
parent
ac5d798cde
commit
048b9c295e
@ -380,6 +380,19 @@ common/views/components/note-menu.vue:
|
||||
delete-confirm: "この投稿を削除しますか?"
|
||||
remote: "投稿元で見る"
|
||||
|
||||
common/views/components/user-menu.vue:
|
||||
mention: "メンション"
|
||||
mute: "ミュート"
|
||||
unmute: "ミュート解除"
|
||||
block: "ブロック"
|
||||
unblock: "ブロック解除"
|
||||
push-to-list: "リストに追加"
|
||||
select-list: "リストを選択してください"
|
||||
list-pushed: "{user}を{list}に追加しました"
|
||||
report-abuse: "スパムを報告"
|
||||
report-abuse-detail: "どのような迷惑行為を行っていますか?"
|
||||
report-abuse-reported: "管理者に報告されました。ご協力ありがとうございました。"
|
||||
|
||||
common/views/components/poll.vue:
|
||||
vote-to: "「{}」に投票する"
|
||||
vote-count: "{}票"
|
||||
@ -1103,6 +1116,7 @@ admin/views/index.vue:
|
||||
federation: "連合"
|
||||
announcements: "お知らせ"
|
||||
hashtags: "ハッシュタグ"
|
||||
abuse: "スパム報告"
|
||||
back-to-misskey: "Misskeyに戻る"
|
||||
|
||||
admin/views/dashboard.vue:
|
||||
@ -1114,6 +1128,13 @@ admin/views/dashboard.vue:
|
||||
this-instance: "このインスタンス"
|
||||
federated: "連合"
|
||||
|
||||
admin/views/abuse.vue:
|
||||
title: "スパム報告"
|
||||
target: "対象"
|
||||
reporter: "報告者"
|
||||
details: "詳細"
|
||||
remove-report: "削除"
|
||||
|
||||
admin/views/instance.vue:
|
||||
instance: "インスタンス"
|
||||
instance-name: "インスタンス名"
|
||||
@ -1384,20 +1405,12 @@ desktop/views/pages/user/user.profile.vue:
|
||||
stalk: "ストークする"
|
||||
stalking: "ストーキングしています"
|
||||
unstalk: "ストーク解除"
|
||||
mute: "ミュートする"
|
||||
muted: "ミュートしています"
|
||||
unmute: "ミュート解除"
|
||||
block: "ブロックする"
|
||||
unblock: "ブロック解除"
|
||||
block-confirm: "このユーザーをブロックしますか?"
|
||||
push-to-a-list: "リストに追加"
|
||||
list-pushed: "{user}を{list}に追加しました。"
|
||||
menu: "メニュー"
|
||||
|
||||
desktop/views/pages/user/user.header.vue:
|
||||
posts: "投稿"
|
||||
following: "フォロー"
|
||||
followers: "フォロワー"
|
||||
mention: "メンション"
|
||||
is-bot: "このアカウントはBotです"
|
||||
years-old: "{age}歳"
|
||||
year: "年"
|
||||
@ -1686,14 +1699,7 @@ mobile/views/pages/user.vue:
|
||||
overview: "概要"
|
||||
timeline: "タイムライン"
|
||||
media: "メディア"
|
||||
mute: "ミュート"
|
||||
unmute: "ミュート解除"
|
||||
block: "ブロック"
|
||||
unblock: "ブロック解除"
|
||||
years-old: "{age}歳"
|
||||
push-to-list: "リストに追加"
|
||||
select-list: "リストを選択してください"
|
||||
list-pushed: "{user}を{list}に追加しました"
|
||||
|
||||
mobile/views/pages/user/home.vue:
|
||||
recent-notes: "最近の投稿"
|
||||
@ -1747,12 +1753,10 @@ deck/deck.user-column.vue:
|
||||
posts: "投稿"
|
||||
following: "フォロー"
|
||||
followers: "フォロワー"
|
||||
mention: "メンション"
|
||||
images: "画像"
|
||||
activity: "アクティビティ"
|
||||
timeline: "タイムライン"
|
||||
pinned-notes: "ピン留めされた投稿"
|
||||
push-to-a-list: "リストに追加"
|
||||
|
||||
docs:
|
||||
edit-this-page-on-github: "間違いや改善点を見つけましたか?"
|
||||
|
87
src/client/app/admin/views/abuse.vue
Normal file
87
src/client/app/admin/views/abuse.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="wbjusose">
|
||||
<ui-card>
|
||||
<div slot="title"><fa :icon="faExclamationCircle"/> {{ $t('title') }}</div>
|
||||
<section class="fit-top">
|
||||
<sequential-entrance animation="entranceFromTop" delay="25">
|
||||
<div v-for="report in userReports" :key="report.id" class="haexwsjc">
|
||||
<ui-horizon-group inputs>
|
||||
<ui-input :value="report.user | acct" type="text">
|
||||
<span>{{ $t('target') }}</span>
|
||||
</ui-input>
|
||||
<ui-input :value="report.reporter | acct" type="text">
|
||||
<span>{{ $t('reporter') }}</span>
|
||||
</ui-input>
|
||||
</ui-horizon-group>
|
||||
<ui-textarea :value="report.comment" readonly>
|
||||
<span>{{ $t('details') }}</span>
|
||||
</ui-textarea>
|
||||
<ui-button @click="removeReport(report)">{{ $t('remove-report') }}</ui-button>
|
||||
</div>
|
||||
</sequential-entrance>
|
||||
<ui-button v-if="existMore" @click="fetchUserReports">{{ $t('@.load-more') }}</ui-button>
|
||||
</section>
|
||||
</ui-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../i18n';
|
||||
import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('admin/views/abuse.vue'),
|
||||
|
||||
data() {
|
||||
return {
|
||||
limit: 10,
|
||||
untilId: undefined,
|
||||
userReports: [],
|
||||
existMore: false,
|
||||
faExclamationCircle
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchUserReports();
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchUserReports() {
|
||||
this.$root.api('admin/abuse-user-reports', {
|
||||
untilId: this.untilId,
|
||||
limit: this.limit + 1
|
||||
}).then(reports => {
|
||||
if (reports.length == this.limit + 1) {
|
||||
reports.pop();
|
||||
this.existMore = true;
|
||||
} else {
|
||||
this.existMore = false;
|
||||
}
|
||||
this.userReports = this.userReports.concat(reports);
|
||||
this.untilId = this.userReports[this.userReports.length - 1].id;
|
||||
});
|
||||
},
|
||||
|
||||
removeReport(report) {
|
||||
this.$root.api('admin/remove-abuse-user-report', {
|
||||
reportId: report.id
|
||||
}).then(() => {
|
||||
this.userReports = this.userReports.filter(r => r.id != report.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.wbjusose
|
||||
@media (min-width 500px)
|
||||
padding 16px
|
||||
|
||||
.haexwsjc
|
||||
padding-bottom 16px
|
||||
border-bottom solid 1px var(--faceDivider)
|
||||
|
||||
</style>
|
@ -27,6 +27,7 @@
|
||||
<li @click="nav('emoji')" :class="{ active: page == 'emoji' }"><fa :icon="faGrin" fixed-width/>{{ $t('emoji') }}</li>
|
||||
<li @click="nav('announcements')" :class="{ active: page == 'announcements' }"><fa icon="broadcast-tower" fixed-width/>{{ $t('announcements') }}</li>
|
||||
<li @click="nav('hashtags')" :class="{ active: page == 'hashtags' }"><fa icon="hashtag" fixed-width/>{{ $t('hashtags') }}</li>
|
||||
<li @click="nav('abuse')" :class="{ active: page == 'abuse' }"><fa :icon="faExclamationCircle" fixed-width/>{{ $t('abuse') }}</li>
|
||||
</ul>
|
||||
<div class="back-to-misskey">
|
||||
<a href="/"><fa :icon="faArrowLeft"/> {{ $t('back-to-misskey') }}</a>
|
||||
@ -45,7 +46,7 @@
|
||||
<div v-if="page == 'announcements'"><x-announcements/></div>
|
||||
<div v-if="page == 'hashtags'"><x-hashtags/></div>
|
||||
<div v-if="page == 'drive'"><x-drive/></div>
|
||||
<div v-if="page == 'update'"></div>
|
||||
<div v-if="page == 'abuse'"><x-abuse/></div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@ -63,7 +64,8 @@ import XAnnouncements from "./announcements.vue";
|
||||
import XHashtags from "./hashtags.vue";
|
||||
import XUsers from "./users.vue";
|
||||
import XDrive from "./drive.vue";
|
||||
import { faHeadset, faArrowLeft, faShareAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import XAbuse from "./abuse.vue";
|
||||
import { faHeadset, faArrowLeft, faShareAlt, faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faGrin } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
// Detect the user agent
|
||||
@ -81,6 +83,7 @@ export default Vue.extend({
|
||||
XHashtags,
|
||||
XUsers,
|
||||
XDrive,
|
||||
XAbuse,
|
||||
},
|
||||
provide: {
|
||||
isMobile
|
||||
@ -94,7 +97,8 @@ export default Vue.extend({
|
||||
faGrin,
|
||||
faArrowLeft,
|
||||
faHeadset,
|
||||
faShareAlt
|
||||
faShareAlt,
|
||||
faExclamationCircle
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
157
src/client/app/common/views/components/user-menu.vue
Normal file
157
src/client/app/common/views/components/user-menu.vue
Normal file
@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<div style="position:initial">
|
||||
<mk-menu :source="source" :items="items" @closed="closed"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import copyToClipboard from '../../../common/scripts/copy-to-clipboard';
|
||||
import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/user-menu.vue'),
|
||||
|
||||
props: ['user', 'source'],
|
||||
|
||||
data() {
|
||||
let menu = [{
|
||||
icon: ['fas', 'at'],
|
||||
text: this.$t('mention'),
|
||||
action: () => {
|
||||
this.$post({ mention: this.user });
|
||||
}
|
||||
}, null, {
|
||||
icon: ['fas', 'list'],
|
||||
text: this.$t('push-to-list'),
|
||||
action: this.pushList
|
||||
}, null, {
|
||||
icon: this.user.isMuted ? ['fas', 'eye'] : ['far', 'eye-slash'],
|
||||
text: this.user.isMuted ? this.$t('unmute') : this.$t('mute'),
|
||||
action: this.toggleMute
|
||||
}, {
|
||||
icon: 'ban',
|
||||
text: this.user.isBlocking ? this.$t('unblock') : this.$t('block'),
|
||||
action: this.toggleBlock
|
||||
}, null, {
|
||||
icon: faExclamationCircle,
|
||||
text: this.$t('report-abuse'),
|
||||
action: this.reportAbuse
|
||||
}];
|
||||
|
||||
return {
|
||||
items: menu
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
closed() {
|
||||
this.$nextTick(() => {
|
||||
this.destroyDom();
|
||||
});
|
||||
},
|
||||
|
||||
async pushList() {
|
||||
const lists = await this.$root.api('users/lists/list');
|
||||
const { canceled, result: listId } = await this.$root.dialog({
|
||||
type: null,
|
||||
title: this.$t('select-list'),
|
||||
select: {
|
||||
items: lists.map(list => ({
|
||||
value: list.id, text: list.title
|
||||
}))
|
||||
},
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
await this.$root.api('users/lists/push', {
|
||||
listId: listId,
|
||||
userId: this.user.id
|
||||
});
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('list-pushed', {
|
||||
user: this.user.name,
|
||||
list: lists.find(l => l.id === listId).title
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
toggleMute() {
|
||||
if (this.user.isMuted) {
|
||||
this.$root.api('mute/delete', {
|
||||
userId: this.user.id
|
||||
}).then(() => {
|
||||
this.user.isMuted = false;
|
||||
}, () => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.$root.api('mute/create', {
|
||||
userId: this.user.id
|
||||
}).then(() => {
|
||||
this.user.isMuted = true;
|
||||
}, () => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
toggleBlock() {
|
||||
if (this.user.isBlocking) {
|
||||
this.$root.api('blocking/delete', {
|
||||
userId: this.user.id
|
||||
}).then(() => {
|
||||
this.user.isBlocking = false;
|
||||
}, () => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.$root.api('blocking/create', {
|
||||
userId: this.user.id
|
||||
}).then(() => {
|
||||
this.user.isBlocking = true;
|
||||
}, () => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async reportAbuse() {
|
||||
const reported = this.$t('report-abuse-reported'); // なぜか後で参照すると null になるので最初にメモリに確保しておく
|
||||
const { canceled, result: comment } = await this.$root.dialog({
|
||||
title: this.$t('report-abuse-detail'),
|
||||
input: true
|
||||
});
|
||||
if (canceled) return;
|
||||
this.$root.api('users/report-abuse', {
|
||||
userId: this.user.id,
|
||||
comment: comment
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
text: reported
|
||||
});
|
||||
}, e => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
@ -49,9 +49,6 @@
|
||||
<b>{{ user.followersCount | number }}</b>
|
||||
<span>{{ $t('followers') }}</span>
|
||||
</div>
|
||||
<div class="mention">
|
||||
<button @click="mention" :title="$t('mention')"><fa icon="at"/></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pinned" v-if="user.pinnedNotes && user.pinnedNotes.length > 0">
|
||||
@ -100,8 +97,7 @@ import parseAcct from '../../../../../../misc/acct/parse';
|
||||
import XColumn from './deck.column.vue';
|
||||
import XNotes from './deck.notes.vue';
|
||||
import XNote from '../../components/note.vue';
|
||||
import Menu from '../../../../common/views/components/menu.vue';
|
||||
import MkUserListsWindow from '../../components/user-lists-window.vue';
|
||||
import XUserMenu from '../../../../common/views/components/user-menu.vue';
|
||||
import { concat } from '../../../../../../prelude/array';
|
||||
import * as ApexCharts from 'apexcharts';
|
||||
|
||||
@ -306,33 +302,10 @@ export default Vue.extend({
|
||||
return promise;
|
||||
},
|
||||
|
||||
mention() {
|
||||
this.$post({ mention: this.user });
|
||||
},
|
||||
|
||||
menu() {
|
||||
let menu = [{
|
||||
icon: 'list',
|
||||
text: this.$t('push-to-a-list'),
|
||||
action: () => {
|
||||
const w = this.$root.new(MkUserListsWindow);
|
||||
w.$once('choosen', async list => {
|
||||
w.close();
|
||||
await this.$root.api('users/lists/push', {
|
||||
listId: list.id,
|
||||
userId: this.user.id
|
||||
});
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
splash: true
|
||||
});
|
||||
});
|
||||
}
|
||||
}];
|
||||
|
||||
this.$root.new(Menu, {
|
||||
this.$root.new(XUserMenu, {
|
||||
source: this.$refs.menu,
|
||||
items: menu
|
||||
user: this.user
|
||||
});
|
||||
},
|
||||
|
||||
@ -459,7 +432,7 @@ export default Vue.extend({
|
||||
|
||||
> .counts
|
||||
display grid
|
||||
grid-template-columns 2fr 2fr 2fr 1fr
|
||||
grid-template-columns 2fr 2fr 2fr
|
||||
margin-top 8px
|
||||
border-top solid var(--lineWidth) var(--faceDivider)
|
||||
|
||||
@ -476,9 +449,6 @@ export default Vue.extend({
|
||||
font-size 80%
|
||||
opacity 0.7
|
||||
|
||||
> .mention
|
||||
display flex
|
||||
|
||||
> *
|
||||
> p.caption
|
||||
margin 0
|
||||
|
@ -36,7 +36,6 @@
|
||||
<span class="notes-count"><b>{{ user.notesCount | number }}</b>{{ $t('posts') }}</span>
|
||||
<router-link :to="user | userPage('following')" class="following clickable"><b>{{ user.followingCount | number }}</b>{{ $t('following') }}</router-link>
|
||||
<router-link :to="user | userPage('followers')" class="followers clickable"><b>{{ user.followersCount | number }}</b>{{ $t('followers') }}</router-link>
|
||||
<button @click="mention" :title="$t('mention')"><fa icon="at"/></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,15 +9,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="action-form">
|
||||
<ui-button @click="user.isMuted ? unmute() : mute()" v-if="$store.state.i.id != user.id">
|
||||
<span v-if="user.isMuted"><fa icon="eye"/> {{ $t('unmute') }}</span>
|
||||
<span v-else><fa :icon="['far', 'eye-slash']"/> {{ $t('mute') }}</span>
|
||||
</ui-button>
|
||||
<ui-button @click="user.isBlocking ? unblock() : block()" v-if="$store.state.i.id != user.id">
|
||||
<span v-if="user.isBlocking"><fa icon="ban"/> {{ $t('unblock') }}</span>
|
||||
<span v-else><fa icon="ban"/> {{ $t('block') }}</span>
|
||||
</ui-button>
|
||||
<ui-button @click="list"><fa icon="list"/> {{ $t('push-to-a-list') }}</ui-button>
|
||||
<ui-button @click="menu" ref="menu">{{ $t('menu') }}</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -25,7 +17,7 @@
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import MkUserListsWindow from '../../components/user-lists-window.vue';
|
||||
import XUserMenu from '../../../../common/views/components/user-menu.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('desktop/views/pages/user/user.profile.vue'),
|
||||
@ -52,72 +44,12 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
mute() {
|
||||
this.$root.api('mute/create', {
|
||||
userId: this.user.id
|
||||
}).then(() => {
|
||||
this.user.isMuted = true;
|
||||
}, () => {
|
||||
alert('error');
|
||||
menu() {
|
||||
this.$root.new(XUserMenu, {
|
||||
source: this.$refs.menu.$el,
|
||||
user: this.user
|
||||
});
|
||||
},
|
||||
|
||||
unmute() {
|
||||
this.$root.api('mute/delete', {
|
||||
userId: this.user.id
|
||||
}).then(() => {
|
||||
this.user.isMuted = false;
|
||||
}, () => {
|
||||
alert('error');
|
||||
});
|
||||
},
|
||||
|
||||
block() {
|
||||
this.$root.dialog({
|
||||
type: 'warning',
|
||||
text: this.$t('block-confirm'),
|
||||
showCancelButton: true
|
||||
}).then(({ canceled }) => {
|
||||
if (canceled) return;
|
||||
|
||||
this.$root.api('blocking/create', {
|
||||
userId: this.user.id
|
||||
}).then(() => {
|
||||
this.user.isBlocking = true;
|
||||
}, () => {
|
||||
alert('error');
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
unblock() {
|
||||
this.$root.api('blocking/delete', {
|
||||
userId: this.user.id
|
||||
}).then(() => {
|
||||
this.user.isBlocking = false;
|
||||
}, () => {
|
||||
alert('error');
|
||||
});
|
||||
},
|
||||
|
||||
list() {
|
||||
const w = this.$root.new(MkUserListsWindow);
|
||||
w.$once('choosen', async list => {
|
||||
w.close();
|
||||
await this.$root.api('users/lists/push', {
|
||||
listId: list.id,
|
||||
userId: this.user.id
|
||||
});
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
title: 'Done!',
|
||||
text: this.$t('list-pushed', {
|
||||
user: this.user.name,
|
||||
list: list.title
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -55,7 +55,6 @@
|
||||
<b>{{ user.followersCount | number }}</b>
|
||||
<i>{{ $t('followers') }}</i>
|
||||
</a>
|
||||
<button @click="mention"><fa icon="at"/></button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@ -81,7 +80,7 @@ import i18n from '../../../i18n';
|
||||
import * as age from 's-age';
|
||||
import parseAcct from '../../../../../misc/acct/parse';
|
||||
import Progress from '../../../common/scripts/loading';
|
||||
import Menu from '../../../common/views/components/menu.vue';
|
||||
import XUserMenu from '../../../common/views/components/user-menu.vue';
|
||||
import XHome from './user/home.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
@ -127,88 +126,10 @@ export default Vue.extend({
|
||||
});
|
||||
},
|
||||
|
||||
mention() {
|
||||
this.$post({ mention: this.user });
|
||||
},
|
||||
|
||||
menu() {
|
||||
let menu = [{
|
||||
icon: ['fas', 'list'],
|
||||
text: this.$t('push-to-list'),
|
||||
action: async () => {
|
||||
const lists = await this.$root.api('users/lists/list');
|
||||
const { canceled, result: listId } = await this.$root.dialog({
|
||||
type: null,
|
||||
title: this.$t('select-list'),
|
||||
select: {
|
||||
items: lists.map(list => ({
|
||||
value: list.id, text: list.title
|
||||
}))
|
||||
},
|
||||
showCancelButton: true
|
||||
});
|
||||
if (canceled) return;
|
||||
await this.$root.api('users/lists/push', {
|
||||
listId: listId,
|
||||
userId: this.user.id
|
||||
});
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
text: this.$t('list-pushed', {
|
||||
user: this.user.name,
|
||||
list: lists.find(l => l.id === listId).title
|
||||
})
|
||||
});
|
||||
}
|
||||
}, null, {
|
||||
icon: this.user.isMuted ? ['fas', 'eye'] : ['far', 'eye-slash'],
|
||||
text: this.user.isMuted ? this.$t('unmute') : this.$t('mute'),
|
||||
action: () => {
|
||||
if (this.user.isMuted) {
|
||||
this.$root.api('mute/delete', {
|
||||
userId: this.user.id
|
||||
}).then(() => {
|
||||
this.user.isMuted = false;
|
||||
}, () => {
|
||||
alert('error');
|
||||
});
|
||||
} else {
|
||||
this.$root.api('mute/create', {
|
||||
userId: this.user.id
|
||||
}).then(() => {
|
||||
this.user.isMuted = true;
|
||||
}, () => {
|
||||
alert('error');
|
||||
});
|
||||
}
|
||||
}
|
||||
}, {
|
||||
icon: 'ban',
|
||||
text: this.user.isBlocking ? this.$t('unblock') : this.$t('block'),
|
||||
action: () => {
|
||||
if (this.user.isBlocking) {
|
||||
this.$root.api('blocking/delete', {
|
||||
userId: this.user.id
|
||||
}).then(() => {
|
||||
this.user.isBlocking = false;
|
||||
}, () => {
|
||||
alert('error');
|
||||
});
|
||||
} else {
|
||||
this.$root.api('blocking/create', {
|
||||
userId: this.user.id
|
||||
}).then(() => {
|
||||
this.user.isBlocking = true;
|
||||
}, () => {
|
||||
alert('error');
|
||||
});
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
this.$root.new(Menu, {
|
||||
this.$root.new(XUserMenu, {
|
||||
source: this.$refs.menu,
|
||||
items: menu
|
||||
user: this.user
|
||||
});
|
||||
},
|
||||
}
|
||||
|
52
src/models/abuse-user-report.ts
Normal file
52
src/models/abuse-user-report.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import * as mongo from 'mongodb';
|
||||
const deepcopy = require('deepcopy');
|
||||
import db from '../db/mongodb';
|
||||
import isObjectId from '../misc/is-objectid';
|
||||
import { pack as packUser } from './user';
|
||||
|
||||
const AbuseUserReport = db.get<IAbuseUserReport>('abuseUserReports');
|
||||
AbuseUserReport.createIndex('userId');
|
||||
AbuseUserReport.createIndex('reporterId');
|
||||
AbuseUserReport.createIndex(['userId', 'reporterId'], { unique: true });
|
||||
export default AbuseUserReport;
|
||||
|
||||
export interface IAbuseUserReport {
|
||||
_id: mongo.ObjectID;
|
||||
createdAt: Date;
|
||||
userId: mongo.ObjectID;
|
||||
reporterId: mongo.ObjectID;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
export const packMany = (
|
||||
reports: (string | mongo.ObjectID | IAbuseUserReport)[]
|
||||
) => {
|
||||
return Promise.all(reports.map(x => pack(x)));
|
||||
};
|
||||
|
||||
export const pack = (
|
||||
report: any
|
||||
) => new Promise<any>(async (resolve, reject) => {
|
||||
let _report: any;
|
||||
|
||||
if (isObjectId(report)) {
|
||||
_report = await AbuseUserReport.findOne({
|
||||
_id: report
|
||||
});
|
||||
} else if (typeof report === 'string') {
|
||||
_report = await AbuseUserReport.findOne({
|
||||
_id: new mongo.ObjectID(report)
|
||||
});
|
||||
} else {
|
||||
_report = deepcopy(report);
|
||||
}
|
||||
|
||||
// Rename _id to id
|
||||
_report.id = _report._id;
|
||||
delete _report._id;
|
||||
|
||||
_report.reporter = await packUser(_report.reporterId, null, { detail: true });
|
||||
_report.user = await packUser(_report.userId, null, { detail: true });
|
||||
|
||||
resolve(_report);
|
||||
});
|
54
src/server/api/endpoints/admin/abuse-user-reports.ts
Normal file
54
src/server/api/endpoints/admin/abuse-user-reports.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
|
||||
import Report, { packMany } from '../../../../models/abuse-user-report';
|
||||
import define from '../../define';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
params: {
|
||||
limit: {
|
||||
validator: $.num.optional.range(1, 100),
|
||||
default: 10
|
||||
},
|
||||
|
||||
sinceId: {
|
||||
validator: $.type(ID).optional,
|
||||
transform: transform,
|
||||
},
|
||||
|
||||
untilId: {
|
||||
validator: $.type(ID).optional,
|
||||
transform: transform,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, (ps) => new Promise(async (res, rej) => {
|
||||
if (ps.sinceId && ps.untilId) {
|
||||
return rej('cannot set sinceId and untilId');
|
||||
}
|
||||
|
||||
const sort = {
|
||||
_id: -1
|
||||
};
|
||||
const query = {} as any;
|
||||
if (ps.sinceId) {
|
||||
sort._id = 1;
|
||||
query._id = {
|
||||
$gt: ps.sinceId
|
||||
};
|
||||
} else if (ps.untilId) {
|
||||
query._id = {
|
||||
$lt: ps.untilId
|
||||
};
|
||||
}
|
||||
|
||||
const reports = await Report
|
||||
.find(query, {
|
||||
limit: ps.limit,
|
||||
sort: sort
|
||||
});
|
||||
|
||||
res(await packMany(reports));
|
||||
}));
|
32
src/server/api/endpoints/admin/remove-abuse-user-report.ts
Normal file
32
src/server/api/endpoints/admin/remove-abuse-user-report.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import $ from 'cafy';
|
||||
import ID, { transform } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import AbuseUserReport from '../../../../models/abuse-user-report';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
params: {
|
||||
reportId: {
|
||||
validator: $.type(ID),
|
||||
transform: transform
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, (ps) => new Promise(async (res, rej) => {
|
||||
const report = await AbuseUserReport.findOne({
|
||||
_id: ps.reportId
|
||||
});
|
||||
|
||||
if (report == null) {
|
||||
return rej('report not found');
|
||||
}
|
||||
|
||||
await AbuseUserReport.remove({
|
||||
_id: report._id
|
||||
});
|
||||
|
||||
res();
|
||||
}));
|
62
src/server/api/endpoints/users/report-abuse.ts
Normal file
62
src/server/api/endpoints/users/report-abuse.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
|
||||
import define from '../../define';
|
||||
import User from '../../../../models/user';
|
||||
import AbuseUserReport from '../../../../models/abuse-user-report';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
'ja-JP': '指定したユーザーを迷惑なユーザーであると報告します。'
|
||||
},
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
params: {
|
||||
userId: {
|
||||
validator: $.type(ID),
|
||||
transform: transform,
|
||||
desc: {
|
||||
'ja-JP': '対象のユーザーのID',
|
||||
'en-US': 'Target user ID'
|
||||
}
|
||||
},
|
||||
|
||||
comment: {
|
||||
validator: $.str.range(1, 3000),
|
||||
desc: {
|
||||
'ja-JP': '迷惑行為の詳細'
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
||||
// Lookup user
|
||||
const user = await User.findOne({
|
||||
_id: ps.userId
|
||||
}, {
|
||||
fields: {
|
||||
_id: true
|
||||
}
|
||||
});
|
||||
|
||||
if (user === null) {
|
||||
return rej('user not found');
|
||||
}
|
||||
|
||||
if (user._id.equals(me._id)) {
|
||||
return rej('cannot report yourself');
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
return rej('cannot report admin');
|
||||
}
|
||||
|
||||
await AbuseUserReport.insert({
|
||||
createdAt: new Date(),
|
||||
userId: user._id,
|
||||
reporterId: me._id,
|
||||
comment: ps.comment
|
||||
});
|
||||
|
||||
res();
|
||||
}));
|
Loading…
Reference in New Issue
Block a user