1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-07-15 16:24:53 +02:00

Compare commits

..

40 Commits
6.3.0 ... 6.4.1

Author SHA1 Message Date
syuilo
b7adbbb9bf 6.4.1 2018-08-18 06:01:55 +09:00
syuilo
31aaf559ac Fix bug 2018-08-18 05:46:57 +09:00
syuilo
160185b108 Fix bug 2018-08-18 05:43:09 +09:00
syuilo
1d6b2bd969 6.4.0 2018-08-18 05:21:45 +09:00
syuilo
5df90fdc4a l10n 2018-08-18 05:15:39 +09:00
syuilo
607a5326d9 Merge branch 'master' of https://github.com/syuilo/misskey 2018-08-18 05:05:48 +09:00
syuilo
2cbd35acc4 🎨 2018-08-18 05:05:39 +09:00
Aya Morisawa
9a0224ee21 🎨 2018-08-18 05:00:54 +09:00
syuilo
2beedc5978 Merge branch 'master' of https://github.com/syuilo/misskey 2018-08-18 04:52:15 +09:00
syuilo
6d53e64798 Improve control panel 2018-08-18 04:52:06 +09:00
syuilo
3478cdea08 Merge pull request #2304 from acid-chicken/acid-chicken-patch-4
Update README.md
2018-08-18 04:39:36 +09:00
Acid Chicken (硫酸鶏)
b360f2ed45 Update README.md 2018-08-18 04:34:44 +09:00
syuilo
7b0c4c29b4 Merge pull request #2303 from syuilo/update-readme
Update README.md
2018-08-18 04:29:29 +09:00
Aya Morisawa
03f5d7575c Update README.md 2018-08-18 04:27:58 +09:00
syuilo
e922d8904c Merge branch 'master' of https://github.com/syuilo/misskey 2018-08-18 04:22:00 +09:00
syuilo
45b55a8c98 Update rate limit setting 2018-08-18 04:21:49 +09:00
syuilo
8973b76bda Merge pull request #2302 from syuilo/kao-to-faces
Fix #2301
2018-08-18 04:20:57 +09:00
Aya Morisawa
41cf856e26 Fix #2301 2018-08-18 04:13:25 +09:00
syuilo
b7aeb10304 Merge pull request #2300 from syuilo/delete-autogen
Delete .autogen
2018-08-18 04:05:47 +09:00
Aya Morisawa
b8d9c1aa45 Delete .autogen 2018-08-18 04:05:00 +09:00
Aya Morisawa
d7dbe503b6 🎨 2018-08-18 03:59:56 +09:00
syuilo
c625dd074a Merge pull request #2299 from syuilo/new-kao
Add new kao
2018-08-18 03:56:30 +09:00
Aya Morisawa
f8a977f1c0 Add new kao 2018-08-18 03:55:45 +09:00
syuilo
c35e760fd3 Merge branch 'master' of https://github.com/syuilo/misskey 2018-08-18 03:52:46 +09:00
syuilo
bc34ac82cf Show some charts in control panel 2018-08-18 03:52:24 +09:00
syuilo
98f25c9159 6.3.2 2018-08-18 03:37:56 +09:00
syuilo
ae9c13df66 6.3.1 2018-08-18 03:37:10 +09:00
syuilo
9f8d21b2bc Fix #2298 2018-08-18 03:36:13 +09:00
syuilo
cd09fa5a28 Merge pull request #2297 from syuilo/update-readme
:v::sunny::umbrella::cloud::snowflake:✌️
2018-08-17 23:20:58 +09:00
Aya Morisawa
9ca9757418 :v::sunny::umbrella::cloud::snowflake:✌️ 2018-08-17 23:19:24 +09:00
syuilo
1fae2ffc37 Merge branch 'master' of https://github.com/syuilo/misskey 2018-08-17 23:07:01 +09:00
syuilo
71657ddb98 🎨 2018-08-17 23:06:58 +09:00
syuilo
63845e0ba7 Merge pull request #2294 from acid-chicken/patch-1
Resolve #2291
2018-08-17 22:38:40 +09:00
Acid Chicken (硫酸鶏)
6781b68d33 Delete README.foot.md 2018-08-17 22:37:11 +09:00
Acid Chicken (硫酸鶏)
d058eff59a Update and rename README.head.md to README.md 2018-08-17 22:36:38 +09:00
syuilo
42addfed85 Merge pull request #2292 from syuilo/patch-2271
Resolve #2271
2018-08-17 21:43:15 +09:00
greenkeeper[bot]
0dd6494ab9 fix(package): update url-loader to version 1.1.1 2018-08-17 21:41:42 +09:00
Aya Morisawa
9f4c6a3aef Resolve #2271 2018-08-17 21:40:08 +09:00
Aya Morisawa
ad0d06c0d8 #332 2018-08-17 21:29:06 +09:00
syuilo
fbf43c1450 Fix bug 2018-08-17 19:38:39 +09:00
26 changed files with 765 additions and 242 deletions

View File

@@ -1,25 +0,0 @@
:four_leaf_clover: Copyright
----------------------------------------------------------------
> Copyright (c) 2014-2018 syuilo
Misskey is an open-source software licensed under the [GNU AGPLv3](LICENSE).
[![][agpl-3.0-badge]][AGPL-3.0]
[agpl-3.0]: https://www.gnu.org/licenses/agpl-3.0.en.html
[agpl-3.0-badge]: https://img.shields.io/badge/license-AGPL--3.0-444444.svg?style=flat-square
[travis-link]: https://travis-ci.org/syuilo/misskey
[travis-badge]: http://img.shields.io/travis/syuilo/misskey/master.svg?style=flat-square
[dependencies-link]: https://david-dm.org/syuilo/misskey
[dependencies-badge]: https://img.shields.io/david/syuilo/misskey.svg?style=flat-square
[backer-url]: #backers
[backer-badge]: https://opencollective.com/misskey/backers/badge.svg
[backers-image]: https://opencollective.com/misskey/backers.svg
[sponsor-url]: #sponsors
[sponsor-badge]: https://opencollective.com/misskey/sponsors/badge.svg
[sponsors-image]: https://opencollective.com/misskey/sponsors.svg
[support-url]: https://opencollective.com/misskey#support
[syuilo-link]: https://syuilo.com
[syuilo-icon]: https://avatars2.githubusercontent.com/u/4439005?v=3&s=70

View File

@@ -1,45 +0,0 @@
<img src="https://github.com/syuilo/misskey/blob/b3f42e62af698a67c2250533c437569559f1fdf9/src/himasaku/resources/himasaku.png?raw=true" align="right" width="320px"/>
[![Misskey](/assets/title.png)](https://misskey.xyz/)
================================================================
[![][travis-badge]][travis-link]
[![][dependencies-badge]][dependencies-link]
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) [![Greenkeeper badge](https://badges.greenkeeper.io/syuilo/misskey.svg)](https://greenkeeper.io/)
**Microblogging. Redefined.**
**[Misskey](https://misskey.xyz)** is a completely open source,
ultimately sophisticated professional microblogging software.
<a href="https://www.patreon.com/syuilo"><img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" alt="Become a Patron!" width="160" /></a>
![](https://ja.mstdn.wiki/images/e/ed/Deck.jpg)
:sparkles: Features
----------------------------------------------------------------
* Rich text contents
* Reactions
* User lists
* Customizable column view (called MisskeyDeck)
* and widgets!
* Private messages
* ActivityPub support
and more! You can see it with your own eyes at [misskey.xyz](https://misskey.xyz).
:package: Create your instance
----------------------------------------------------------------
If you want to run your own instance of Misskey,
please see [Setup and installation guide](./docs/setup.en.md).
:wrench: Contribute
----------------------------------------------------------------
**[PR](https://github.com/syuilo/misskey/pulls)s welcome!**
If you want to...
* i18n ... please see [Translation guide](./docs/translate.en.md).
* l10n ... please visit https://crowdin.com/project/misskey
:heart: Backers & Sponsors
----------------------------------------------------------------

View File

@@ -7,10 +7,12 @@
[![][dependencies-badge]][dependencies-link]
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) [![Greenkeeper badge](https://badges.greenkeeper.io/syuilo/misskey.svg)](https://greenkeeper.io/)
**Microblogging. Redefined.**
Sophisticated microblogging platform, evolving forever.
**[Misskey](https://misskey.xyz)** is a completely open source,
ultimately sophisticated professional microblogging software.
[Misskey](https://misskey.xyz) is a decentralized microblogging platform born on Earth.
Since it exists within the Fediverse (a universe where various social media platforms are organized),
it is mutually linked with other social media platforms.
Why don't you take a short break from the hustle and bustle of the city, and dive into a new Internet?
<a href="https://www.patreon.com/syuilo"><img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" alt="Become a Patron!" width="160" /></a>
@@ -28,7 +30,7 @@ ultimately sophisticated professional microblogging software.
and more! You can see it with your own eyes at [misskey.xyz](https://misskey.xyz).
:package: Create your instance
:package: Create your own instance
----------------------------------------------------------------
If you want to run your own instance of Misskey,
please see [Setup and installation guide](./docs/setup.en.md).
@@ -43,6 +45,7 @@ If you want to...
:heart: Backers & Sponsors
----------------------------------------------------------------
<!-- PATREON_START -->
<table>
<tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJoIjoxMDAsInciOjEwMH0%3D/patreon-media/p/user/619786/32cf01444db24e578cd1982c197f6fc6/1?token-time=2145916800&token-hash=tB1e_r8RlZ5sFL0KV_e8dugapxatNBRK1Z3h67TO1g8%3D"></td>
@@ -71,6 +74,7 @@ If you want to...
<td><a href="https://www.patreon.com/gutfuckllc">gutfuckllc</a></td>
</tr>
</table>
<!-- PATREON_END -->
:four_leaf_clover: Copyright
----------------------------------------------------------------

View File

@@ -914,6 +914,7 @@ desktop/views/pages/admin/admin.dashboard.vue:
original-users: "このインスタンスのユーザー"
all-notes: "全てのノート"
original-notes: "このインスタンスのノート"
invite: "招待"
desktop/views/pages/admin/admin.suspend-user.vue:
suspend-user: "ユーザーの凍結"
@@ -925,6 +926,21 @@ desktop/views/pages/admin/admin.unsuspend-user.vue:
unsuspend: "凍結の解除"
unsuspended: "凍結を解除しました"
desktop/views/pages/admin/admin.verify-user.vue:
verify-user: "ユーザーの公式アカウント設定"
verify: "公式アカウントにする"
verified: "公式アカウントにしました"
desktop/views/pages/admin/admin.notes-chart.vue:
title: "投稿"
local: "ローカル"
remote: "リモート"
desktop/views/pages/admin/admin.users-chart.vue:
title: "ユーザー"
local: "ローカル"
remote: "リモート"
desktop/views/pages/deck/deck.tl-column.vue:
is-media-only: "メディア投稿のみ"
is-media-view: "メディアビュー"

View File

@@ -1,8 +1,8 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "6.3.0",
"clientVersion": "1.0.8483",
"version": "6.4.1",
"clientVersion": "1.0.8520",
"codename": "nighthike",
"main": "./built/index.js",
"private": true,
@@ -201,7 +201,7 @@
"typescript": "2.9.2",
"typescript-eslint-parser": "18.0.0",
"uglify-es": "3.3.9",
"url-loader": "1.1.0",
"url-loader": "1.1.1",
"uuid": "3.3.2",
"v-animate-css": "0.0.2",
"vue": "2.5.17",

View File

@@ -0,0 +1,10 @@
const faces = [
'(=^・・^=)',
'v(\'ω\')v',
'🐡( \'-\' 🐡 )フグパンチ!!!!',
'🖕(´・_・`)🖕',
'(。><。)',
'(Δ・x・Δ)'
];
export default () => faces[Math.floor(Math.random() * faces.length)];

View File

@@ -1,9 +0,0 @@
const kaos = [
'(=^・・^=)',
'v(\'ω\')v',
'🐡( \'-\' 🐡 )フグパンチ!!!!',
'🖕(´・_・`)🖕',
'(。><。)'
];
export default () => kaos[Math.floor(Math.random() * kaos.length)];

View File

@@ -58,7 +58,7 @@
import Vue from 'vue';
import insertTextAtCursor from 'insert-text-at-cursor';
import * as XDraggable from 'vuedraggable';
import getKao from '../../../common/scripts/get-kao';
import getFace from '../../../common/scripts/get-face';
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
import parse from '../../../../../mfm/parse';
import { host } from '../../../config';
@@ -421,7 +421,7 @@ export default Vue.extend({
},
kao() {
this.text += getKao();
this.text += getFace();
}
}
});

View File

@@ -0,0 +1,145 @@
<template>
<div class="zyknedwtlthezamcjlolyusmipqmjgxz">
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
<defs>
<linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
</linearGradient>
<mask :id="cpuMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
<polygon
:points="cpuPolygonPoints"
fill="#fff"
fill-opacity="0.5"/>
<polyline
:points="cpuPolylinePoints"
fill="none"
stroke="#fff"
stroke-width="0.3"/>
</mask>
</defs>
<rect
x="0" y="0"
:width="viewBoxX" :height="viewBoxY"
:style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`"/>
<text x="1" y="5">CPU <tspan>{{ cpuP }}%</tspan></text>
</svg>
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
<defs>
<linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0">
<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
</linearGradient>
<mask :id="memMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
<polygon
:points="memPolygonPoints"
fill="#fff"
fill-opacity="0.5"/>
<polyline
:points="memPolylinePoints"
fill="none"
stroke="#fff"
stroke-width="0.3"/>
</mask>
</defs>
<rect
x="0" y="0"
:width="viewBoxX" :height="viewBoxY"
:style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`"/>
<text x="1" y="5">MEM <tspan>{{ memP }}%</tspan></text>
</svg>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import * as uuid from 'uuid';
export default Vue.extend({
props: ['connection'],
data() {
return {
viewBoxX: 50,
viewBoxY: 20,
stats: [],
cpuGradientId: uuid(),
cpuMaskId: uuid(),
memGradientId: uuid(),
memMaskId: uuid(),
cpuPolylinePoints: '',
memPolylinePoints: '',
cpuPolygonPoints: '',
memPolygonPoints: '',
cpuP: '',
memP: ''
};
},
mounted() {
this.connection.on('stats', this.onStats);
this.connection.on('statsLog', this.onStatsLog);
this.connection.send({
type: 'requestLog',
id: Math.random().toString()
});
},
beforeDestroy() {
this.connection.off('stats', this.onStats);
this.connection.off('statsLog', this.onStatsLog);
},
methods: {
onStats(stats) {
this.stats.push(stats);
if (this.stats.length > 50) this.stats.shift();
const cpuPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - s.cpu_usage) * this.viewBoxY]);
const memPolylinePoints = this.stats.map((s, i) => [this.viewBoxX - ((this.stats.length - 1) - i), (1 - (s.mem.used / s.mem.total)) * this.viewBoxY]);
this.cpuPolylinePoints = cpuPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
this.memPolylinePoints = memPolylinePoints.map(xy => `${xy[0]},${xy[1]}`).join(' ');
this.cpuPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.cpuPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
this.memPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${this.viewBoxY} ${this.memPolylinePoints} ${this.viewBoxX},${this.viewBoxY}`;
this.cpuP = (stats.cpu_usage * 100).toFixed(0);
this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0);
},
onStatsLog(statsLog) {
statsLog.forEach(stats => this.onStats(stats));
}
}
});
</script>
<style lang="stylus" scoped>
root(isDark)
margin-bottom 16px
> svg
display block
width 50%
float left
&:first-child
padding-right 5px
&:last-child
padding-left 5px
> text
font-size 2px
fill isDark ? rgba(#fff, 0.55) : rgba(#000, 0.55)
> tspan
opacity 0.5
&:after
content ""
display block
clear both
.zyknedwtlthezamcjlolyusmipqmjgxz[data-darkmode]
root(true)
.zyknedwtlthezamcjlolyusmipqmjgxz:not([data-darkmode])
root(false)
</style>

View File

@@ -1,11 +1,14 @@
<template>
<div>
<h1>%i18n:@dashboard%</h1>
<div v-if="stats">
<p><b>%i18n:@all-users%</b>: <span>{{ stats.usersCount | number }}</span></p>
<p><b>%i18n:@original-users%</b>: <span>{{ stats.originalUsersCount | number }}</span></p>
<p><b>%i18n:@all-notes%</b>: <span>{{ stats.notesCount | number }}</span></p>
<p><b>%i18n:@original-notes%</b>: <span>{{ stats.originalNotesCount | number }}</span></p>
<div class="obdskegsannmntldydackcpzezagxqfy card">
<header>%i18n:@dashboard%</header>
<div v-if="stats" class="stats">
<div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div>
<div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div>
<div><b>%fa:pen% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div>
<div><span>%fa:pen% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div>
</div>
<div class="cpu-memory">
<x-cpu-memory :connection="connection"/>
</div>
<div>
<button class="ui" @click="invite">%i18n:@invite%</button>
@@ -16,19 +19,31 @@
<script lang="ts">
import Vue from "vue";
import XCpuMemory from "./admin.cpu-memory.vue";
export default Vue.extend({
components: {
XCpuMemory
},
data() {
return {
stats: null,
inviteCode: null
inviteCode: null,
connection: null,
connectionId: null
};
},
created() {
this.connection = (this as any).os.streams.serverStatsStream.getConnection();
this.connectionId = (this as any).os.streams.serverStatsStream.use();
(this as any).api('stats').then(stats => {
this.stats = stats;
});
},
beforeDestroy() {
(this as any).os.streams.serverStatsStream.dispose(this.connectionId);
},
methods: {
invite() {
(this as any).api('admin/invite').then(x => {
@@ -40,10 +55,26 @@ export default Vue.extend({
</script>
<style lang="stylus" scoped>
h1
margin 0 0 1em 0
padding 0 0 8px 0
font-size 1em
color #555
border-bottom solid 1px #eee
@import '~const.styl'
.obdskegsannmntldydackcpzezagxqfy
> .stats
display flex
justify-content center
margin-bottom 16px
padding 16px
border solid 1px #eee
border-radius 8px
> div
flex 1
text-align center
> *:first-child
display block
color $theme-color
> *:last-child
font-size 70%
</style>

View File

@@ -0,0 +1,81 @@
<template>
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
<polyline
:points="pointsNote"
fill="none"
stroke-width="1"
stroke="#41ddde"/>
<polyline
:points="pointsReply"
fill="none"
stroke-width="1"
stroke="#f7796c"/>
<polyline
:points="pointsRenote"
fill="none"
stroke-width="1"
stroke="#a1de41"/>
<polyline
:points="pointsTotal"
fill="none"
stroke-width="1"
stroke="#555"
stroke-dasharray="2 2"/>
</svg>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
data: {
required: true
},
type: {
type: String,
required: true
}
},
data() {
return {
chart: this.data,
viewBoxX: 365,
viewBoxY: 70,
pointsNote: null,
pointsReply: null,
pointsRenote: null,
pointsTotal: null
};
},
created() {
this.chart.forEach(d => {
d.notes = this.type == 'local' ? d.localNotes : d.remoteNotes;
d.replies = this.type == 'local' ? d.localReplies : d.remoteReplies;
d.renotes = this.type == 'local' ? d.localRenotes : d.remoteRenotes;
});
this.chart.forEach(d => {
d.total = d.notes + d.replies + d.renotes;
});
const peak = Math.max.apply(null, this.chart.map(d => d.total));
if (peak != 0) {
const data = this.chart.slice().reverse();
this.pointsNote = data.map((d, i) => `${i},${(1 - (d.notes / peak)) * this.viewBoxY}`).join(' ');
this.pointsReply = data.map((d, i) => `${i},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
this.pointsRenote = data.map((d, i) => `${i},${(1 - (d.renotes / peak)) * this.viewBoxY}`).join(' ');
this.pointsTotal = data.map((d, i) => `${i},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
}
}
});
</script>
<style lang="stylus" scoped>
svg
display block
padding 10px
width 100%
</style>

View File

@@ -0,0 +1,39 @@
<template>
<div class="card">
<header>%i18n:@title%</header>
<div class="card">
<header>%i18n:@local%</header>
<x-chart v-if="data" :data="data" type="local"/>
</div>
<div class="card">
<header>%i18n:@remote%</header>
<x-chart v-if="data" :data="data" type="remote"/>
</div>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import XChart from "./admin.notes-chart.chart.vue";
export default Vue.extend({
components: {
XChart
},
data() {
return {
data: null
};
},
created() {
(this as any).api('aggregation/notes').then(res => {
this.data = res;
});
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div class="card">
<header>%i18n:@suspend-user%</header>
<input v-model="username" type="text" class="ui"/>
<button class="ui" @click="suspendUser" :disabled="suspending">%i18n:@suspend%</button>

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div class="card">
<header>%i18n:@unsuspend-user%</header>
<input v-model="username" type="text" class="ui"/>
<button class="ui" @click="unsuspendUser" :disabled="unsuspending">%i18n:@unsuspend%</button>

View File

@@ -0,0 +1,53 @@
<template>
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
<polyline
:points="points"
fill="none"
stroke-width="1"
stroke="#555"/>
</svg>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
props: {
data: {
required: true
},
type: {
type: String,
required: true
}
},
data() {
return {
chart: this.data,
viewBoxX: 365,
viewBoxY: 70,
points: null
};
},
created() {
this.chart.forEach(d => {
d.count = this.type == 'local' ? d.local : d.remote;
});
const peak = Math.max.apply(null, this.chart.map(d => d.count));
if (peak != 0) {
const data = this.chart.slice().reverse();
this.points = data.map((d, i) => `${i},${(1 - (d.count / peak)) * this.viewBoxY}`).join(' ');
}
}
});
</script>
<style lang="stylus" scoped>
svg
display block
padding 10px
width 100%
</style>

View File

@@ -0,0 +1,39 @@
<template>
<div class="card">
<header>%i18n:@title%</header>
<div class="card">
<header>%i18n:@local%</header>
<x-chart v-if="data" :data="data" type="local"/>
</div>
<div class="card">
<header>%i18n:@remote%</header>
<x-chart v-if="data" :data="data" type="remote"/>
</div>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import XChart from "./admin.users-chart.chart.vue";
export default Vue.extend({
components: {
XChart
},
data() {
return {
data: null
};
},
created() {
(this as any).api('aggregation/users').then(res => {
this.data = res;
});
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
</style>

View File

@@ -0,0 +1,51 @@
<template>
<div class="card">
<header>%i18n:@verify-user%</header>
<input v-model="username" type="text" class="ui"/>
<button class="ui" @click="verifyUser" :disabled="verifying">%i18n:@verify%</button>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import parseAcct from "../../../../../../misc/acct/parse";
export default Vue.extend({
data() {
return {
username: null,
verifying: false
};
},
methods: {
async verifyUser() {
this.verifying = true;
const user = await (this as any).os.api(
"users/show",
parseAcct(this.username)
);
await (this as any).os.api("admin/verify-user", {
userId: user.id
});
this.verifying = false;
(this as any).os.apis.dialog({ text: "%i18n:@verified%" });
}
}
});
</script>
<style lang="stylus" scoped>
@import '~const.styl'
header
margin 10px 0
button
margin 16px 0
</style>

View File

@@ -9,12 +9,15 @@
</ul>
</nav>
<main>
<div v-if="page == 'dashboard'">
<div v-show="page == 'dashboard'">
<x-dashboard/>
<x-users-chart/>
<x-notes-chart/>
</div>
<div v-if="page == 'users'">
<x-suspend-user/>
<x-unsuspend-user/>
<x-verify-user/>
</div>
<div v-if="page == 'drive'"></div>
<div v-if="page == 'update'"></div>
@@ -27,12 +30,18 @@ import Vue from "vue";
import XDashboard from "./admin.dashboard.vue";
import XSuspendUser from "./admin.suspend-user.vue";
import XUnsuspendUser from "./admin.unsuspend-user.vue";
import XVerifyUser from "./admin.verify-user.vue";
import XUsersChart from "./admin.users-chart.vue";
import XNotesChart from "./admin.notes-chart.vue";
export default Vue.extend({
components: {
XDashboard,
XSuspendUser,
XUnsuspendUser
XUnsuspendUser,
XVerifyUser,
XUsersChart,
XNotesChart
},
data() {
return {
@@ -47,7 +56,7 @@ export default Vue.extend({
});
</script>
<style lang="stylus" scoped>
<style lang="stylus">
@import '~const.styl'
.mk-admin
@@ -90,13 +99,23 @@ export default Vue.extend({
width 100%
padding 16px 32px
header
margin 10px 0
> div
> div
max-width 800px
.card
padding 32px
background #fff
box-shadow 0 2px 8px rgba(#000, 0.1)
button
margin 16px 0
position absolute
right 0
&:not(:last-child)
margin-bottom 16px
> header
margin 0 0 1em 0
padding 0 0 8px 0
font-size 1em
color #555
border-bottom solid 1px #eee
</style>

View File

@@ -95,7 +95,7 @@ export default Vue.extend({
callbackUrl: this.cb,
permission: this.permission
}).then(() => {
location.href = '/apps';
location.href = '/dev/apps';
}).catch(() => {
alert('アプリの作成に失敗しました。再度お試しください。');
});

View File

@@ -56,7 +56,7 @@ import Vue from 'vue';
import insertTextAtCursor from 'insert-text-at-cursor';
import * as XDraggable from 'vuedraggable';
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
import getKao from '../../../common/scripts/get-kao';
import getFace from '../../../common/scripts/get-face';
import parse from '../../../../../mfm/parse';
import { host } from '../../../config';
@@ -313,7 +313,7 @@ export default Vue.extend({
},
kao() {
this.text += getKao();
this.text += getFace();
}
}
});

View File

@@ -0,0 +1,46 @@
import $ from 'cafy';
import ID from '../../../../misc/cafy-id';
import getParams from '../../get-params';
import User from '../../../../models/user';
export const meta = {
desc: {
ja: '指定したユーザーを公式アカウントにします。',
en: 'Mark a user as verified.'
},
requireCredential: true,
requireAdmin: true,
params: {
userId: $.type(ID).note({
desc: {
ja: '対象のユーザーID',
en: 'The user ID which you want to verify'
}
}),
}
};
export default (params: any) => new Promise(async (res, rej) => {
const [ps, psErr] = getParams(meta, params);
if (psErr) return rej(psErr);
const user = await User.findOne({
_id: ps.userId
});
if (user == null) {
return rej('user not found');
}
await User.findOneAndUpdate({
_id: user._id
}, {
$set: {
isVerified: true
}
});
res();
});

View File

@@ -0,0 +1,116 @@
import Note from '../../../../models/note';
export const meta = {
requireCredential: true,
requireAdmin: true
};
/**
* Aggregate notes
*/
export default (params: any) => new Promise(async (res, rej) => {
const query = [{
$match: {
createdAt: {
$gt: new Date(new Date().setFullYear(new Date().getFullYear() - 1))
}
}
}, {
$project: {
renoteId: '$renoteId',
replyId: '$replyId',
user: '$_user',
createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
}
}, {
$project: {
date: {
year: { $year: '$createdAt' },
month: { $month: '$createdAt' },
day: { $dayOfMonth: '$createdAt' }
},
type: {
$cond: {
if: { $ne: ['$renoteId', null] },
then: 'renote',
else: {
$cond: {
if: { $ne: ['$replyId', null] },
then: 'reply',
else: 'note'
}
}
}
},
origin: {
$cond: {
if: { $eq: ['$user.host', null] },
then: 'local',
else: 'remote'
}
}
}
}, {
$group: {
_id: {
date: '$date',
type: '$type',
origin: '$origin'
},
count: { $sum: 1 }
}
}, {
$group: {
_id: '$_id.date',
data: {
$addToSet: {
type: '$_id.type',
origin: '$_id.origin',
count: '$count'
}
}
}
}] as any;
const datas = await Note.aggregate(query);
datas.forEach((data: any) => {
data.date = data._id;
delete data._id;
data.localNotes = (data.data.filter((x: any) => x.type == 'note' && x.origin == 'local')[0] || { count: 0 }).count;
data.localRenotes = (data.data.filter((x: any) => x.type == 'renote' && x.origin == 'local')[0] || { count: 0 }).count;
data.localReplies = (data.data.filter((x: any) => x.type == 'reply' && x.origin == 'local')[0] || { count: 0 }).count;
data.remoteNotes = (data.data.filter((x: any) => x.type == 'note' && x.origin == 'remote')[0] || { count: 0 }).count;
data.remoteRenotes = (data.data.filter((x: any) => x.type == 'renote' && x.origin == 'remote')[0] || { count: 0 }).count;
data.remoteReplies = (data.data.filter((x: any) => x.type == 'reply' && x.origin == 'remote')[0] || { count: 0 }).count;
delete data.data;
});
const graph = [];
for (let i = 0; i < 365; i++) {
const day = new Date(new Date().setDate(new Date().getDate() - i));
const data = datas.filter((d: any) =>
d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate()
)[0];
if (data) {
graph.push(data);
} else {
graph.push({
date: { year: day.getFullYear(), month: day.getMonth() + 1, day: day.getDate() },
localNotes: 0,
localRenotes: 0,
localReplies: 0,
remoteNotes: 0,
remoteRenotes: 0,
remoteReplies: 0
});
}
}
res(graph);
});

View File

@@ -1,84 +0,0 @@
import $ from 'cafy';
import Note from '../../../../models/note';
/**
* Aggregate notes
*/
export default (params: any) => new Promise(async (res, rej) => {
// Get 'limit' parameter
const [limit = 365, limitErr] = $.num.optional.range(1, 365).get(params.limit);
if (limitErr) return rej('invalid limit param');
const datas = await Note
.aggregate([
{ $project: {
renoteId: '$renoteId',
replyId: '$replyId',
createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
}},
{ $project: {
date: {
year: { $year: '$createdAt' },
month: { $month: '$createdAt' },
day: { $dayOfMonth: '$createdAt' }
},
type: {
$cond: {
if: { $ne: ['$renoteId', null] },
then: 'renote',
else: {
$cond: {
if: { $ne: ['$replyId', null] },
then: 'reply',
else: 'note'
}
}
}
}}
},
{ $group: { _id: {
date: '$date',
type: '$type'
}, count: { $sum: 1 } } },
{ $group: {
_id: '$_id.date',
data: { $addToSet: {
type: '$_id.type',
count: '$count'
}}
} }
]);
datas.forEach((data: any) => {
data.date = data._id;
delete data._id;
data.notes = (data.data.filter((x: any) => x.type == 'note')[0] || { count: 0 }).count;
data.renotes = (data.data.filter((x: any) => x.type == 'renote')[0] || { count: 0 }).count;
data.replies = (data.data.filter((x: any) => x.type == 'reply')[0] || { count: 0 }).count;
delete data.data;
});
const graph = [];
for (let i = 0; i < limit; i++) {
const day = new Date(new Date().setDate(new Date().getDate() - i));
const data = datas.filter((d: any) =>
d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate()
)[0];
if (data) {
graph.push(data);
} else {
graph.push({
notes: 0,
renotes: 0,
replies: 0
});
}
}
res(graph);
});

View File

@@ -1,54 +1,91 @@
import $ from 'cafy';
import User from '../../../../models/user';
export const meta = {
requireCredential: true,
requireAdmin: true
};
/**
* Aggregate users
*/
export default (params: any) => new Promise(async (res, rej) => {
// Get 'limit' parameter
const [limit = 365, limitErr] = $.num.optional.range(1, 365).get(params.limit);
if (limitErr) return rej('invalid limit param');
const users = await User
.find({}, {
sort: {
_id: -1
},
fields: {
_id: false,
createdAt: true,
deletedAt: true
const query = [{
$match: {
createdAt: {
$gt: new Date(new Date().setFullYear(new Date().getFullYear() - 1))
}
});
}
}, {
$project: {
host: '$host',
createdAt: { $add: ['$createdAt', 9 * 60 * 60 * 1000] } // Convert into JST
}
}, {
$project: {
date: {
year: { $year: '$createdAt' },
month: { $month: '$createdAt' },
day: { $dayOfMonth: '$createdAt' }
},
origin: {
$cond: {
if: { $eq: ['$host', null] },
then: 'local',
else: 'remote'
}
}
}
}, {
$group: {
_id: {
date: '$date',
origin: '$origin'
},
count: { $sum: 1 }
}
}, {
$group: {
_id: '$_id.date',
data: {
$addToSet: {
type: '$_id.type',
origin: '$_id.origin',
count: '$count'
}
}
}
}] as any;
const datas = await User.aggregate(query);
datas.forEach((data: any) => {
data.date = data._id;
delete data._id;
data.local = (data.data.filter((x: any) => x.origin == 'local')[0] || { count: 0 }).count;
data.remote = (data.data.filter((x: any) => x.origin == 'remote')[0] || { count: 0 }).count;
delete data.data;
});
const graph = [];
for (let i = 0; i < limit; i++) {
let dayStart = new Date(new Date().setDate(new Date().getDate() - i));
dayStart = new Date(dayStart.setMilliseconds(0));
dayStart = new Date(dayStart.setSeconds(0));
dayStart = new Date(dayStart.setMinutes(0));
dayStart = new Date(dayStart.setHours(0));
for (let i = 0; i < 365; i++) {
const day = new Date(new Date().setDate(new Date().getDate() - i));
let dayEnd = new Date(new Date().setDate(new Date().getDate() - i));
dayEnd = new Date(dayEnd.setMilliseconds(999));
dayEnd = new Date(dayEnd.setSeconds(59));
dayEnd = new Date(dayEnd.setMinutes(59));
dayEnd = new Date(dayEnd.setHours(23));
// day = day.getTime();
const data = datas.filter((d: any) =>
d.date.year == day.getFullYear() && d.date.month == day.getMonth() + 1 && d.date.day == day.getDate()
)[0];
const total = users.filter(u =>
u.createdAt < dayEnd && (u.deletedAt == null || u.deletedAt > dayEnd)
).length;
const created = users.filter(u =>
u.createdAt < dayEnd && u.createdAt > dayStart
).length;
graph.push({
total: total,
created: created
});
if (data) {
graph.push(data);
} else {
graph.push({
date: { year: day.getFullYear(), month: day.getMonth() + 1, day: day.getDate() },
local: 0,
remote: 0
});
}
}
res(graph);

View File

@@ -16,8 +16,7 @@ export const meta = {
limit: {
duration: ms('1hour'),
max: 300,
minInterval: ms('1second')
max: 300
},
kind: 'note-write',

View File

@@ -34,7 +34,7 @@ export default async (ctx: Koa.Context) => {
const meta = await Meta.findOne({});
if (meta.disableRegistration) {
if (meta && meta.disableRegistration) {
if (invitationCode == null || typeof invitationCode != 'string') {
ctx.status = 400;
return;